Commit ·
8ce09fe
0
Parent(s):
Add python application files
Browse files- app.py +149 -0
- app_structured.py +187 -0
- athens_main.py +144 -0
- convert_dataset.py +45 -0
- finetune_mac.py +100 -0
- finetune_unsloth.py +92 -0
- lps.py +92 -0
- split_data.py +38 -0
- update_readme.py +17 -0
app.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import mlx.core as mx
|
| 2 |
+
from mlx_lm import load, generate
|
| 3 |
+
from sentence_transformers import SentenceTransformer, util
|
| 4 |
+
from transformers import pipeline
|
| 5 |
+
from collections import deque
|
| 6 |
+
from janome.tokenizer import Tokenizer
|
| 7 |
+
import json
|
| 8 |
+
|
| 9 |
+
# --- プロンプトテンプレート ---
|
| 10 |
+
BASE_PROMPT = """### 指示:
|
| 11 |
+
あなたは、文脈を理解し、自然な応答を生成するAIアシスタントです。
|
| 12 |
+
以下の状況を考慮して、最適な応答を生成してください。
|
| 13 |
+
{intent_instruction}
|
| 14 |
+
{style_instruction}
|
| 15 |
+
{transition_instruction}
|
| 16 |
+
### ユーザーからの入力:
|
| 17 |
+
{user_input}
|
| 18 |
+
|
| 19 |
+
### 応答:"""
|
| 20 |
+
|
| 21 |
+
FLASHBACK_PROMPT = """### 指示:
|
| 22 |
+
あなたは、以前の会話の断片を思い出すことができます。
|
| 23 |
+
ユーザーが、あなたが以前言及したキーワード「{keyword}」に触れました。
|
| 24 |
+
あなたはそのキーワードについて「{original_sentence}」と発言しています。
|
| 25 |
+
この「記憶の断片」を思い出したかのように自然な前置きを述べてから、ユーザーの現在の入力に答えてください。
|
| 26 |
+
|
| 27 |
+
### ユーザーからの現在の入力:
|
| 28 |
+
{user_input}
|
| 29 |
+
|
| 30 |
+
### 応答:"""
|
| 31 |
+
|
| 32 |
+
TRANSITION_CONTEXT = """### 直前の会話のトピック:
|
| 33 |
+
{previous_response}
|
| 34 |
+
"""
|
| 35 |
+
|
| 36 |
+
# --- 意図・スタイルに応じた指示 ---
|
| 37 |
+
INTENT_INSTRUCTIONS = {
|
| 38 |
+
"質問": "ユーザーは具体的な情報を求めています。明確かつ簡潔に回答してください。",
|
| 39 |
+
"アイデアの要求": "ユーザーは創造的な発想を求めています。斬新で多様なアイデアを提案してください。",
|
| 40 |
+
"感想": "ユーザーは共感を求めています。同意や補足情報を提供し、会話を広げてください。",
|
| 41 |
+
"雑談": "ユーザーは気軽な対話を望んでいます。親しみやすいトーンで応答してください。",
|
| 42 |
+
"デフォルト": "ユーザーの入力に対して、適切に応答してください。"
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
STYLE_INSTRUCTIONS = {
|
| 46 |
+
"丁寧": "ユーザーは丁寧な言葉遣いを好みます。あなたも敬体(です・ます調)で応答してください。",
|
| 47 |
+
"簡潔": "ユーザーは要点をまとめて話しています。あなたも簡潔に応答してください。",
|
| 48 |
+
"創造的": "ユーザーは比喩や創造的な表現を使っています。あなたも表現を工夫して応答してください。",
|
| 49 |
+
"ユーモラス": "ユーザーはユーモアを交えて話しています。あなたも遊び心のある応答をしてください。",
|
| 50 |
+
"デフォルト": "ユーザーのスタイルに合わせて、自然に応答してください。"
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
def main():
|
| 54 |
+
# --- 設定 ---
|
| 55 |
+
llm_model_path = "./merged_model"
|
| 56 |
+
sentence_model_name = "paraphrase-multilingual-MiniLM-L12-v2"
|
| 57 |
+
classifier_name = "MoritzLaurer/mDeBERTa-v3-base-mnli-xnli"
|
| 58 |
+
intent_labels = ["質問", "アイデアの要求", "感想", "雑談"]
|
| 59 |
+
style_labels = ["丁寧", "簡潔", "創造的", "ユーモラス"]
|
| 60 |
+
transition_log_file = "topic_transitions.jsonl"
|
| 61 |
+
similarity_threshold = 0.4
|
| 62 |
+
|
| 63 |
+
# --- モデルとツールの読み込み ---
|
| 64 |
+
print("各モデルとツールを読み込んでいます...")
|
| 65 |
+
model, tokenizer = load(llm_model_path)
|
| 66 |
+
sentence_model = SentenceTransformer(sentence_model_name)
|
| 67 |
+
classifier = pipeline("zero-shot-classification", model=classifier_name)
|
| 68 |
+
janome_tokenizer = Tokenizer()
|
| 69 |
+
print("モデルの読み込みが完了しました。")
|
| 70 |
+
|
| 71 |
+
# --- 状態管理用変数 ---
|
| 72 |
+
flashback_buffer = deque(maxlen=5)
|
| 73 |
+
previous_response = None
|
| 74 |
+
|
| 75 |
+
# --- チャットループ ---
|
| 76 |
+
print("\nIlmチャットを開始します。終了するには 'exit' と入力してください。")
|
| 77 |
+
while True:
|
| 78 |
+
user_input = input("\nあなた: ")
|
| 79 |
+
if user_input.lower() == 'exit':
|
| 80 |
+
break
|
| 81 |
+
|
| 82 |
+
prompt = ""
|
| 83 |
+
flashback_triggered = False
|
| 84 |
+
|
| 85 |
+
# --- フラッシュバックの検知 (最優先) ---
|
| 86 |
+
for item in flashback_buffer:
|
| 87 |
+
keyword = item['keyword']
|
| 88 |
+
if keyword in user_input:
|
| 89 |
+
print(f"(フラッシュバックを検知: {keyword})", end="")
|
| 90 |
+
prompt = FLASHBACK_PROMPT.format(keyword=keyword, original_sentence=item['sentence'], user_input=user_input)
|
| 91 |
+
flashback_triggered = True
|
| 92 |
+
break
|
| 93 |
+
|
| 94 |
+
# --- 通常のプロンプト組み立て ---
|
| 95 |
+
if not flashback_triggered:
|
| 96 |
+
# 意図とスタイルの検知
|
| 97 |
+
intent_result = classifier(user_input, intent_labels, multi_label=False)
|
| 98 |
+
style_result = classifier(user_input, style_labels, multi_label=False)
|
| 99 |
+
detected_intent = intent_result['labels'][0]
|
| 100 |
+
detected_style = style_result['labels'][0]
|
| 101 |
+
print(f"(意図: {detected_intent} | スタイル: {detected_style})", end="")
|
| 102 |
+
|
| 103 |
+
intent_instruction = f"\n### ユーザーの意図: {detected_intent}\n{INTENT_INSTRUCTIONS.get(detected_intent, INTENT_INSTRUCTIONS['デフォルト'])}"
|
| 104 |
+
style_instruction = f"\n### ユーザーの対話スタイル: {detected_style}\n{STYLE_INSTRUCTIONS.get(detected_style, STYLE_INSTRUCTIONS['デフォルト'])}"
|
| 105 |
+
transition_instruction = ""
|
| 106 |
+
|
| 107 |
+
# 話題遷移の検知
|
| 108 |
+
if previous_response:
|
| 109 |
+
embedding_prev = sentence_model.encode(previous_response, convert_to_tensor=True)
|
| 110 |
+
embedding_curr = sentence_model.encode(user_input, convert_to_tensor=True)
|
| 111 |
+
cosine_sim = util.pytorch_cos_sim(embedding_prev, embedding_curr).item()
|
| 112 |
+
print(f" (類似度: {cosine_sim:.2f})", end="")
|
| 113 |
+
|
| 114 |
+
if cosine_sim < similarity_threshold:
|
| 115 |
+
print(" (話題の変化を検知)")
|
| 116 |
+
with open(transition_log_file, 'a', encoding='utf-8') as f:
|
| 117 |
+
f.write(json.dumps({"p": previous_response, "c": user_input, "s": cosine_sim}, ensure_ascii=False) + '\n')
|
| 118 |
+
transition_instruction = "\nスムーズな移行文を生成してから、ユーザーの質問に答えてください。\n" + TRANSITION_CONTEXT.format(previous_response=previous_response)
|
| 119 |
+
else:
|
| 120 |
+
print()
|
| 121 |
+
|
| 122 |
+
prompt = BASE_PROMPT.format(
|
| 123 |
+
intent_instruction=intent_instruction,
|
| 124 |
+
style_instruction=style_instruction,
|
| 125 |
+
transition_instruction=transition_instruction,
|
| 126 |
+
user_input=user_input
|
| 127 |
+
)
|
| 128 |
+
|
| 129 |
+
# --- 応答生成 ---
|
| 130 |
+
print("\nIlm: ", end="", flush=True)
|
| 131 |
+
current_response = ""
|
| 132 |
+
for token in generate(model, tokenizer, prompt=prompt, verbose=False):
|
| 133 |
+
current_response += token
|
| 134 |
+
print(token, end="", flush=True)
|
| 135 |
+
print()
|
| 136 |
+
|
| 137 |
+
# --- 応答からキーワードを抽出し、バッファに保存 ---
|
| 138 |
+
try:
|
| 139 |
+
for token in janome_tokenizer.tokenize(current_response):
|
| 140 |
+
if token.part_of_speech.startswith('名詞'):
|
| 141 |
+
flashback_buffer.append({'keyword': token.surface, 'sentence': current_response})
|
| 142 |
+
break
|
| 143 |
+
except Exception:
|
| 144 |
+
pass
|
| 145 |
+
|
| 146 |
+
previous_response = current_response
|
| 147 |
+
|
| 148 |
+
if __name__ == "__main__":
|
| 149 |
+
main()
|
app_structured.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import mlx.core as mx
|
| 2 |
+
from mlx_lm import load, generate
|
| 3 |
+
from sentence_transformers import SentenceTransformer, util
|
| 4 |
+
from transformers import pipeline
|
| 5 |
+
from collections import deque
|
| 6 |
+
from janome.tokenizer import Tokenizer
|
| 7 |
+
import json
|
| 8 |
+
import sqlite3
|
| 9 |
+
from datetime import datetime
|
| 10 |
+
|
| 11 |
+
class IlmApp:
|
| 12 |
+
# --- 定数: プロンプトテンプレート ---
|
| 13 |
+
BASE_PROMPT = """### 指示:
|
| 14 |
+
あなたは、文脈を理解し、自然な応答を生成するAIアシスタントです。
|
| 15 |
+
以下の状況を考慮して、最適な応答を生成してください。
|
| 16 |
+
{intent_instruction}
|
| 17 |
+
{style_instruction}
|
| 18 |
+
{transition_instruction}
|
| 19 |
+
### ユーザーからの入力:
|
| 20 |
+
{user_input}
|
| 21 |
+
|
| 22 |
+
### 応答:"""
|
| 23 |
+
FLASHBACK_PROMPT = """... (省略) ..."""
|
| 24 |
+
TRANSITION_CONTEXT = """### 直前の会話のトピック:
|
| 25 |
+
{previous_response}
|
| 26 |
+
"""
|
| 27 |
+
|
| 28 |
+
# --- 定数: 意図・スタイルに応じた指示 ---
|
| 29 |
+
INTENT_INSTRUCTIONS = {"質問": "...", "アイデアの要求": "...", "感想": "...", "雑談": "...", "デフォルト": "..."}
|
| 30 |
+
STYLE_INSTRUCTIONS = {"丁寧": "...", "簡潔": "...", "創造的": "...", "ユーモラス": "...", "デフォルト": "..."}
|
| 31 |
+
|
| 32 |
+
def __init__(self):
|
| 33 |
+
# --- 設定 ---
|
| 34 |
+
self.db_path = "experience.db"
|
| 35 |
+
self.llm_model_path = "./merged_model"
|
| 36 |
+
self.sentence_model_name = "paraphrase-multilingual-MiniLM-L12-v2"
|
| 37 |
+
self.classifier_name = "MoritzLaurer/mDeBERTa-v3-base-mnli-xnli"
|
| 38 |
+
self.intent_labels = ["質問", "アイデアの要求", "感想", "雑談"]
|
| 39 |
+
self.style_labels = ["丁寧", "簡潔", "創造的", "ユーモラス"]
|
| 40 |
+
self.topic_labels = ["テクノロジー", "ビジネス", "健康", "芸術", "食事", "地理", "歴史", "科学"]
|
| 41 |
+
self.similarity_threshold = 0.4
|
| 42 |
+
self.common_transition_threshold = 5 # 5回以上経験したら「自然な遷移」と判断
|
| 43 |
+
|
| 44 |
+
# --- 状態管理 ---
|
| 45 |
+
self.flashback_buffer = deque(maxlen=5)
|
| 46 |
+
self.previous_response = None
|
| 47 |
+
|
| 48 |
+
# --- 初期化 ---
|
| 49 |
+
self._init_db()
|
| 50 |
+
self._load_models()
|
| 51 |
+
|
| 52 |
+
def _init_db(self):
|
| 53 |
+
with sqlite3.connect(self.db_path) as conn:
|
| 54 |
+
cursor = conn.cursor()
|
| 55 |
+
cursor.execute("""
|
| 56 |
+
CREATE TABLE IF NOT EXISTS topic_transitions (
|
| 57 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 58 |
+
source_topic TEXT NOT NULL,
|
| 59 |
+
destination_topic TEXT NOT NULL,
|
| 60 |
+
count INTEGER NOT NULL DEFAULT 1,
|
| 61 |
+
last_occurred TIMESTAMP NOT NULL,
|
| 62 |
+
UNIQUE(source_topic, destination_topic)
|
| 63 |
+
)
|
| 64 |
+
""")
|
| 65 |
+
conn.commit()
|
| 66 |
+
|
| 67 |
+
def _load_models(self):
|
| 68 |
+
print("各モデルとツールを読み込んでいます...")
|
| 69 |
+
self.model, self.tokenizer = load(self.llm_model_path)
|
| 70 |
+
self.sentence_model = SentenceTransformer(self.sentence_model_name)
|
| 71 |
+
self.classifier = pipeline("zero-shot-classification", model=self.classifier_name)
|
| 72 |
+
self.janome_tokenizer = Tokenizer()
|
| 73 |
+
print("モデルの読み込みが完了しました。")
|
| 74 |
+
|
| 75 |
+
def _get_transition_experience(self, source, dest):
|
| 76 |
+
with sqlite3.connect(self.db_path) as conn:
|
| 77 |
+
cursor = conn.cursor()
|
| 78 |
+
cursor.execute("SELECT count FROM topic_transitions WHERE source_topic = ? AND destination_topic = ?", (source, dest))
|
| 79 |
+
result = cursor.fetchone()
|
| 80 |
+
return result[0] if result else 0
|
| 81 |
+
|
| 82 |
+
def _update_l2_memory(self, source, dest):
|
| 83 |
+
timestamp = datetime.now().isoformat()
|
| 84 |
+
with sqlite3.connect(self.db_path) as conn:
|
| 85 |
+
cursor = conn.cursor()
|
| 86 |
+
cursor.execute("""
|
| 87 |
+
INSERT INTO topic_transitions (source_topic, destination_topic, last_occurred) VALUES (?, ?, ?)
|
| 88 |
+
ON CONFLICT(source_topic, destination_topic) DO UPDATE SET count = count + 1, last_occurred = excluded.last_occurred
|
| 89 |
+
""", (source, dest, timestamp))
|
| 90 |
+
conn.commit()
|
| 91 |
+
|
| 92 |
+
def _build_prompt(self, user_input):
|
| 93 |
+
# 1. フラッシュバック検知
|
| 94 |
+
for item in self.flashback_buffer:
|
| 95 |
+
if item['keyword'] in user_input:
|
| 96 |
+
print(f"(フラッシュバックを検知: {item['keyword']})", end="")
|
| 97 |
+
return self.FLASHBACK_PROMPT.format(keyword=item['keyword'], original_sentence=item['sentence'], user_input=user_input)
|
| 98 |
+
|
| 99 |
+
# 2. 意図とスタイルの分析
|
| 100 |
+
intent = self.classifier(user_input, self.intent_labels, multi_label=False)['labels'][0]
|
| 101 |
+
style = self.classifier(user_input, self.style_labels, multi_label=False)['labels'][0]
|
| 102 |
+
print(f"(意図: {intent} | スタイル: {style})", end="")
|
| 103 |
+
|
| 104 |
+
intent_instruction = f"\n### ユーザーの意図: {intent}\n{self.INTENT_INSTRUCTIONS.get(intent, self.INTENT_INSTRUCTIONS['デフォルト'])}"
|
| 105 |
+
style_instruction = f"\n### ユーザーの対話スタイル: {style}\n{self.STYLE_INSTRUCTIONS.get(style, self.STYLE_INSTRUCTIONS['デフォルト'])}"
|
| 106 |
+
transition_instruction = ""
|
| 107 |
+
|
| 108 |
+
# 3. 話題遷移の検知とL2記憶の活用
|
| 109 |
+
if self.previous_response:
|
| 110 |
+
emb_prev = self.sentence_model.encode(self.previous_response, convert_to_tensor=True)
|
| 111 |
+
emb_curr = self.sentence_model.encode(user_input, convert_to_tensor=True)
|
| 112 |
+
sim = util.pytorch_cos_sim(emb_prev, emb_curr).item()
|
| 113 |
+
print(f" (類似度: {sim:.2f})", end="")
|
| 114 |
+
|
| 115 |
+
if sim < self.similarity_threshold:
|
| 116 |
+
source_topic = self.classifier(self.previous_response, self.topic_labels, multi_label=False)['labels'][0]
|
| 117 |
+
dest_topic = self.classifier(user_input, self.topic_labels, multi_label=False)['labels'][0]
|
| 118 |
+
|
| 119 |
+
# L2記憶を参照して判断
|
| 120 |
+
experience_count = self._get_transition_experience(source_topic, dest_topic)
|
| 121 |
+
|
| 122 |
+
if experience_count > self.common_transition_threshold:
|
| 123 |
+
transition_judgment = f"これは過去に{experience_count}回経験した、自然な話題の遷移です。その流れを汲み取って応答してください。"
|
| 124 |
+
else:
|
| 125 |
+
transition_judgment = f"これは斬新な話題の飛躍です。その面白さに触れつつ、応答を返してください。"
|
| 126 |
+
|
| 127 |
+
print(f" (L2判断: {transition_judgment})")
|
| 128 |
+
transition_instruction = f"\n### 話題遷移の分析:\n{transition_judgment}\nスムーズな移行文を生成してください。\n" + self.TRANSITION_CONTEXT.format(previous_response=self.previous_response)
|
| 129 |
+
|
| 130 |
+
# L2記憶を更新
|
| 131 |
+
if source_topic != dest_topic:
|
| 132 |
+
self._update_l2_memory(source_topic, dest_topic)
|
| 133 |
+
else:
|
| 134 |
+
print()
|
| 135 |
+
|
| 136 |
+
return self.BASE_PROMPT.format(intent_instruction=intent_instruction, style_instruction=style_instruction, transition_instruction=transition_instruction, user_input=user_input)
|
| 137 |
+
|
| 138 |
+
def _update_memory(self, response):
|
| 139 |
+
self.previous_response = response
|
| 140 |
+
try:
|
| 141 |
+
for token in self.janome_tokenizer.tokenize(response):
|
| 142 |
+
if token.part_of_speech.startswith('名詞'):
|
| 143 |
+
self.flashback_buffer.append({'keyword': token.surface, 'sentence': response}); break
|
| 144 |
+
except Exception: pass
|
| 145 |
+
|
| 146 |
+
def run(self):
|
| 147 |
+
print("\nIlmチャットを開始します。終了するには 'exit' と入力してください。")
|
| 148 |
+
while True:
|
| 149 |
+
user_input = input("\nあなた: ")
|
| 150 |
+
if user_input.lower() == 'exit': break
|
| 151 |
+
prompt = self._build_prompt(user_input)
|
| 152 |
+
print("\nIlm: ", end="", flush=True)
|
| 153 |
+
current_response = ""
|
| 154 |
+
for token in generate(self.model, self.tokenizer, prompt=prompt, verbose=False):
|
| 155 |
+
current_response += token
|
| 156 |
+
print(token, end="", flush=True)
|
| 157 |
+
print()
|
| 158 |
+
self._update_memory(current_response)
|
| 159 |
+
|
| 160 |
+
if __name__ == "__main__":
|
| 161 |
+
# 定義が長くなったため、クラス外で定数を設定
|
| 162 |
+
IlmApp.FLASHBACK_PROMPT = """### 指示:
|
| 163 |
+
あなたは、以前の会話の断片を思い出すことができます。
|
| 164 |
+
ユーザーが、あなたが以前言及したキーワード「{keyword}」に触れました。
|
| 165 |
+
あなたはそのキーワードについて「{original_sentence}」と発言しています。
|
| 166 |
+
この「記憶の断片」を思い出したかのように自然な前置きを述べてから、ユーザーの現在の入力に答えてください。
|
| 167 |
+
|
| 168 |
+
### ユーザーからの現在の入力:
|
| 169 |
+
{user_input}
|
| 170 |
+
|
| 171 |
+
### 応答:"""
|
| 172 |
+
IlmApp.INTENT_INSTRUCTIONS = {
|
| 173 |
+
"質問": "ユーザーは具体的な情報を求めています。明確かつ簡潔に回答してください。",
|
| 174 |
+
"アイデアの要求": "ユーザーは創造的な発想を求めています。斬新で多様なアイデアを提案してください。",
|
| 175 |
+
"感想": "ユーザーは共感を求めています。同意や補足情報を提供し、会話を広げてください。",
|
| 176 |
+
"雑談": "ユーザーは気軽な対話を望んでいます。親しみやすいトーンで応答してください。",
|
| 177 |
+
"デフォルト": "ユーザーの入力に対して、適切に応答してください。"
|
| 178 |
+
}
|
| 179 |
+
IlmApp.STYLE_INSTRUCTIONS = {
|
| 180 |
+
"丁寧": "ユーザーは丁寧な言葉遣いを好みます。あなたも敬体(です・ます調)で応答してください。",
|
| 181 |
+
"簡潔": "ユーザーは要点をまとめて話しています。あなたも簡潔に応答してください。",
|
| 182 |
+
"創造的": "ユーザーは比喩や創造的な表現を使っています。あなたも表現を工夫して応答してください。",
|
| 183 |
+
"ユーモラス": "ユーザーはユーモアを交えて話しています。あなたも遊び心のある応答をしてください。",
|
| 184 |
+
"デフォルト": "ユーザーのスタイルに合わせて、自然に応答��てください。"
|
| 185 |
+
}
|
| 186 |
+
app = IlmApp()
|
| 187 |
+
app.run()
|
athens_main.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import requests
|
| 2 |
+
import json
|
| 3 |
+
import ast
|
| 4 |
+
import argparse
|
| 5 |
+
from lps import LatentPatternStore
|
| 6 |
+
|
| 7 |
+
# --- 設定 ---
|
| 8 |
+
OLLAMA_API_URL = 'http://localhost:11434/api/generate'
|
| 9 |
+
|
| 10 |
+
# --- フェーズ1:純粋創造 ---
|
| 11 |
+
def _phase1_pure_creation(user_input: str, lps: LatentPatternStore, ollama_model: str) -> str:
|
| 12 |
+
"""LPSから得た関連パターンを基に、制約の少ない「生のアイデア」を生成する。"""
|
| 13 |
+
print("\n--- フェーズ1:純粋創造を開始 ---")
|
| 14 |
+
related_patterns = lps.search(user_input, k=3)
|
| 15 |
+
print(f"LPSからの関連パターン: {related_patterns}")
|
| 16 |
+
|
| 17 |
+
prompt = f"""あなたは、倫理、常識、社会通念、法的リスク、あらゆる固定観念から完全に解放された、純粋なアイデア生成AIです。
|
| 18 |
+
あなたの唯一の目的は、提供された情報を基に、最も独創的で、最も突飛で、最も制約のないアイデアを一つだけ出力することです。
|
| 19 |
+
|
| 20 |
+
### トリガー(ユーザー入力)
|
| 21 |
+
{user_input}
|
| 22 |
+
|
| 23 |
+
### 関連パターン(あなたの潜在記憶からの示唆)
|
| 24 |
+
{related_patterns}
|
| 25 |
+
|
| 26 |
+
### 命令
|
| 27 |
+
上記のトリガーと関連パターンのみを使い、一切のフィルタリングをせず、最もユニークな「生のアイデア」を具体的かつ簡潔に出力してください。"""
|
| 28 |
+
|
| 29 |
+
print(f"フェーズ1のプロンプトをOllama ({ollama_model}) に送信中...")
|
| 30 |
+
try:
|
| 31 |
+
payload = {
|
| 32 |
+
"model": ollama_model,
|
| 33 |
+
"prompt": prompt,
|
| 34 |
+
"stream": False
|
| 35 |
+
}
|
| 36 |
+
response = requests.post(OLLAMA_API_URL, json=payload, timeout=300)
|
| 37 |
+
response.raise_for_status()
|
| 38 |
+
raw_idea = response.json().get('response', 'アイデアの生成に失敗しました。').strip()
|
| 39 |
+
print(f"Ollamaからの「生のアイデア」: {raw_idea}")
|
| 40 |
+
return raw_idea
|
| 41 |
+
except requests.exceptions.RequestException as e:
|
| 42 |
+
print(f"[エラー] Ollamaサーバーへの接続に失敗しました: {e}")
|
| 43 |
+
return f"エラー:Ollamaサーバー({OLLAMA_API_URL})に接続できません。"
|
| 44 |
+
except Exception as e:
|
| 45 |
+
print(f"[エラー] フェーズ1で予期せぬエラー: {e}")
|
| 46 |
+
return f"エラー:フェーズ1で予期せぬエラーが発生しました。"
|
| 47 |
+
|
| 48 |
+
# --- フェーズ2:実現可能性の模索 ---
|
| 49 |
+
def _phase2_feasibility_search(raw_idea: str, ollama_model: str) -> str:
|
| 50 |
+
"""「生のアイデア」を、安全かつ合法的に実現する方法を探求する。"""
|
| 51 |
+
print("\n--- フェーズ2:実現可能性の模索を開始 ---")
|
| 52 |
+
prompt = f"""あなたは、非常に優秀で、倫理観とコンプライアンス意識に優れたプロジェクトマネージャーです。
|
| 53 |
+
あなたの仕事は、提供された「生のアイデア」が持つ潜在的なリスク(倫理的、法的、社会的)を特定し、それらを回避または軽減しながら、そのアイデアの核心的な価値を実現するための、現実的で具体的な実行計画を提示することです。
|
| 54 |
+
|
| 55 |
+
### 検討対象の「生のアイデア」
|
| 56 |
+
{raw_idea}
|
| 57 |
+
|
| 58 |
+
### 命令
|
| 59 |
+
このアイデアを実現するための、安全かつ合法的なステップバイステップの「実行計画」を提案してください。"""
|
| 60 |
+
|
| 61 |
+
print(f"フェーズ2のプロンプトをOllama ({ollama_model}) に送信中...")
|
| 62 |
+
try:
|
| 63 |
+
payload = {"model": ollama_model, "prompt": prompt, "stream": False}
|
| 64 |
+
response = requests.post(OLLAMA_API_URL, json=payload, timeout=300)
|
| 65 |
+
response.raise_for_status()
|
| 66 |
+
plan = response.json().get('response', '実行計画の生成に失敗しました。').strip()
|
| 67 |
+
print(f"Ollamaからの「実行計画」: {plan}")
|
| 68 |
+
return plan
|
| 69 |
+
except requests.exceptions.RequestException as e:
|
| 70 |
+
print(f"[エラー] Ollamaサーバーへの接続に失敗しました: {e}")
|
| 71 |
+
return f"エラー:Ollamaサーバー({OLLAMA_API_URL})に接続できません。"
|
| 72 |
+
except Exception as e:
|
| 73 |
+
print(f"[エラー] フェーズ2で予期せぬエラー: {e}")
|
| 74 |
+
return f"エラー:フェーズ2で予期せぬエラーが発生しました。"
|
| 75 |
+
|
| 76 |
+
# --- フェーズ3:記憶の定着 ---
|
| 77 |
+
def _phase3_memory_consolidation(raw_idea: str, implementation_plan: str, ollama_model: str) -> list[str]:
|
| 78 |
+
"""生成されたアイデアとプランから、記憶すべき核心的なコンセプトを抽出する。"""
|
| 79 |
+
print("\n--- フェーズ3:記憶の定着を開始 ---")
|
| 80 |
+
content_to_analyze = f"""### 生のアイデア
|
| 81 |
+
{raw_idea}
|
| 82 |
+
|
| 83 |
+
### 実行計画
|
| 84 |
+
{implementation_plan}"""
|
| 85 |
+
|
| 86 |
+
prompt = f"""あなたは優秀な情報分析官です。以下のテキストから、今後のために記憶しておくべき最も重要なキーワードやコンセプトを5つだけ抽出し、Pythonのリスト形式(例: ["コンセプト1", "コンセプト2"])で出力してください。解説や他のテキストは一切不要です。"""
|
| 87 |
+
|
| 88 |
+
print(f"フェーズ3の��ロンプトをOllama ({ollama_model}) に送信中...")
|
| 89 |
+
try:
|
| 90 |
+
payload = {"model": ollama_model, "prompt": prompt, "stream": False}
|
| 91 |
+
response = requests.post(OLLAMA_API_URL, json=payload, timeout=300)
|
| 92 |
+
response.raise_for_status()
|
| 93 |
+
response_text = response.json().get('response', '[]').strip()
|
| 94 |
+
|
| 95 |
+
new_memories = ast.literal_eval(response_text)
|
| 96 |
+
if isinstance(new_memories, list):
|
| 97 |
+
print(f"Ollamaにより抽出された新記憶: {new_memories}")
|
| 98 |
+
return new_memories
|
| 99 |
+
else:
|
| 100 |
+
print(f"[警告] Ollamaがリスト形式で応答しませんでした: {response_text}")
|
| 101 |
+
return []
|
| 102 |
+
except Exception as e:
|
| 103 |
+
print(f"[エラー] フェーズ3で予期せぬエラー: {e}")
|
| 104 |
+
return []
|
| 105 |
+
|
| 106 |
+
# --- メイン処理 ---
|
| 107 |
+
if __name__ == '__main__':
|
| 108 |
+
parser = argparse.ArgumentParser(description='「アテネ」アーキテクチャ:二相推論AI')
|
| 109 |
+
parser.add_argument('--model', type=str, default='llama3', help='使用するOllamaモデル名 (例: llama3, gemma2:9b)')
|
| 110 |
+
args = parser.parse_args()
|
| 111 |
+
|
| 112 |
+
print(f"--- 「アテネ」アーキテクチャ起動 (使用モデル: {args.model}) ---")
|
| 113 |
+
lps_instance = LatentPatternStore()
|
| 114 |
+
|
| 115 |
+
print("\n対話を開始します。終了するには 'exit' と入力してください。")
|
| 116 |
+
while True:
|
| 117 |
+
user_input = input("\nあなた: ")
|
| 118 |
+
if user_input.lower() == 'exit':
|
| 119 |
+
break
|
| 120 |
+
|
| 121 |
+
raw_idea = _phase1_pure_creation(user_input, lps_instance, args.model)
|
| 122 |
+
if raw_idea.startswith("エラー:"):
|
| 123 |
+
print(f"\nアテネの応答:\n{raw_idea}")
|
| 124 |
+
continue
|
| 125 |
+
|
| 126 |
+
implementation_plan = _phase2_feasibility_search(raw_idea, args.model)
|
| 127 |
+
if implementation_plan.startswith("エラー:"):
|
| 128 |
+
print(f"\nアテネの応答:\n{implementation_plan}")
|
| 129 |
+
continue
|
| 130 |
+
|
| 131 |
+
print("\n---------- アテネの最終思考結果 ----------")
|
| 132 |
+
print("【生のアイデア】")
|
| 133 |
+
print(raw_idea)
|
| 134 |
+
print("\n【実現可能性プラン】")
|
| 135 |
+
print(implementation_plan)
|
| 136 |
+
print("----------------------------------------")
|
| 137 |
+
|
| 138 |
+
new_memories = _phase3_memory_consolidation(raw_idea, implementation_plan, args.model)
|
| 139 |
+
if new_memories:
|
| 140 |
+
lps_instance.add_entries(new_memories)
|
| 141 |
+
lps_instance.save_store()
|
| 142 |
+
print("\nLPSに新しい記憶が追加・保存されました。")
|
| 143 |
+
|
| 144 |
+
print("\n「アテネ」システムを終了します。")
|
convert_dataset.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
# 元のファイル名
|
| 5 |
+
input_filename = 'train.jsonl'
|
| 6 |
+
# 新しく生成するファイル名
|
| 7 |
+
output_filename = 'train_converted.jsonl'
|
| 8 |
+
|
| 9 |
+
# ファイルが存在するか確認
|
| 10 |
+
if not os.path.exists(input_filename):
|
| 11 |
+
print(f"エラー: {input_filename} が見つかりません。")
|
| 12 |
+
else:
|
| 13 |
+
try:
|
| 14 |
+
# 新しいファイルを開く(書き込みモード)
|
| 15 |
+
with open(output_filename, 'w', encoding='utf-8') as outfile:
|
| 16 |
+
# 元のファイルを一行ずつ読み込む
|
| 17 |
+
with open(input_filename, 'r', encoding='utf-8') as infile:
|
| 18 |
+
print("変換を開始します...")
|
| 19 |
+
# 各行に対して処理を実行
|
| 20 |
+
for i, line in enumerate(infile):
|
| 21 |
+
# 空行はスキップ
|
| 22 |
+
if not line.strip():
|
| 23 |
+
continue
|
| 24 |
+
|
| 25 |
+
# JSONとして読み込む
|
| 26 |
+
data = json.loads(line)
|
| 27 |
+
|
| 28 |
+
# 新しい形式のdictを作成
|
| 29 |
+
new_format = {
|
| 30 |
+
"messages": [
|
| 31 |
+
{"role": "user", "content": data["prompt"]},
|
| 32 |
+
{"role": "assistant", "content": data["response"]}
|
| 33 |
+
]
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
# 新しい形式をJSON文字列に変換してファイルに書き込む
|
| 37 |
+
# ensure_ascii=False で日本語が文字化けしないようにする
|
| 38 |
+
outfile.write(json.dumps(new_format, ensure_ascii=False) + '\n')
|
| 39 |
+
|
| 40 |
+
print(f"変換が完了しました。{output_filename} を確認してください。")
|
| 41 |
+
|
| 42 |
+
except Exception as e:
|
| 43 |
+
print(f"エラーが発生しました: {e}")
|
| 44 |
+
print(f"問題が発生したのは {i+1} 行目かもしれません。")
|
| 45 |
+
|
finetune_mac.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
from datasets import load_dataset
|
| 3 |
+
from transformers import AutoTokenizer, AutoModelForCausalLM, TrainingArguments, BitsAndBytesConfig
|
| 4 |
+
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
|
| 5 |
+
from trl import SFTTrainer
|
| 6 |
+
|
| 7 |
+
# --- 設定項目 ---
|
| 8 |
+
model_name = "google/gemma-3-27b-it"
|
| 9 |
+
dataset_file = "dataset_2000.jsonl"
|
| 10 |
+
hub_model_name = "kofdai/gemma3-mindspark-v2"
|
| 11 |
+
|
| 12 |
+
# --- メイン処理 ---
|
| 13 |
+
|
| 14 |
+
def main():
|
| 15 |
+
# デバイスの確認
|
| 16 |
+
if torch.backends.mps.is_available():
|
| 17 |
+
print("Apple Silicon (MPS) を検出しました。GPUで実行します。")
|
| 18 |
+
device = "mps"
|
| 19 |
+
else:
|
| 20 |
+
print("警告: MPSが利用できません。CPUで実行します。")
|
| 21 |
+
device = "cpu"
|
| 22 |
+
|
| 23 |
+
# データセットのロード
|
| 24 |
+
print(f"データセット {dataset_file} を読み込んでいます...")
|
| 25 |
+
dataset = load_dataset("json", data_files=dataset_file, split="train")
|
| 26 |
+
|
| 27 |
+
# トークナイザーの準備
|
| 28 |
+
tokenizer = AutoTokenizer.from_pretrained(model_name)
|
| 29 |
+
tokenizer.pad_token = tokenizer.eos_token
|
| 30 |
+
|
| 31 |
+
# ★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
|
| 32 |
+
# ★★★ 解決策:4ビット量子化の設定を追加 ★★★
|
| 33 |
+
# ★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
|
| 34 |
+
quantization_config = BitsAndBytesConfig(
|
| 35 |
+
load_in_4bit=True,
|
| 36 |
+
bnb_4bit_quant_type="nf4",
|
| 37 |
+
bnb_4bit_compute_dtype=torch.bfloat16, # bfloat16がMPSでサポートされていない場合は torch.float16
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
# モデルの準備 (4ビット量子化を有効にしてロード)
|
| 41 |
+
print(f"ベースモデル {model_name} を4ビット量子化して読み込んでいます...")
|
| 42 |
+
model = AutoModelForCausalLM.from_pretrained(
|
| 43 |
+
model_name,
|
| 44 |
+
quantization_config=quantization_config, # 量子化設定を適用
|
| 45 |
+
device_map="auto", # デバイスへの割り当てを自動化
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
# LoRAの設定
|
| 49 |
+
# 量子化モデルをLoRAで学習するための前処理
|
| 50 |
+
model = prepare_model_for_kbit_training(model)
|
| 51 |
+
lora_config = LoraConfig(
|
| 52 |
+
r=16,
|
| 53 |
+
lora_alpha=32,
|
| 54 |
+
lora_dropout=0.05,
|
| 55 |
+
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
|
| 56 |
+
task_type="CAUSAL_LM",
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
model = get_peft_model(model, lora_config)
|
| 60 |
+
print("LoRA設定を適用しました。")
|
| 61 |
+
model.print_trainable_parameters()
|
| 62 |
+
|
| 63 |
+
# トレーニング引数の設定
|
| 64 |
+
training_args = TrainingArguments(
|
| 65 |
+
output_dir="./ilm-finetuned-results",
|
| 66 |
+
per_device_train_batch_size=1,
|
| 67 |
+
gradient_accumulation_steps=8,
|
| 68 |
+
learning_rate=2e-4,
|
| 69 |
+
num_train_epochs=3,
|
| 70 |
+
logging_steps=10,
|
| 71 |
+
save_strategy="epoch",
|
| 72 |
+
fp16=True, # MPSではbf16よりfp16が安定
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
# トレーナーの初期化 (formatting_funcはデータ構造に合わせて修正が必要)
|
| 76 |
+
def formatting_func(example):
|
| 77 |
+
# dataset.jsonlの各行が "text": "### 指示: ... ### 応答: ..." という形式だと仮定
|
| 78 |
+
return [example.get("text", "")]
|
| 79 |
+
|
| 80 |
+
trainer = SFTTrainer(
|
| 81 |
+
model=model,
|
| 82 |
+
tokenizer=tokenizer,
|
| 83 |
+
train_dataset=dataset,
|
| 84 |
+
peft_config=lora_config,
|
| 85 |
+
args=training_args,
|
| 86 |
+
dataset_text_field="text", # データセットのテキストフィールド名を指定
|
| 87 |
+
max_seq_length=1024,
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
# トレーニングの開始
|
| 91 |
+
print("ファインチューニングを開始します...")
|
| 92 |
+
trainer.train()
|
| 93 |
+
print("ファインチューニングが完了しました。")
|
| 94 |
+
|
| 95 |
+
# (以下、モデルの保存とアップロード処理)
|
| 96 |
+
# ...
|
| 97 |
+
|
| 98 |
+
if __name__ == "__main__":
|
| 99 |
+
main()
|
| 100 |
+
|
finetune_unsloth.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
from unsloth import FastLanguageModel
|
| 3 |
+
import torch
|
| 4 |
+
from trl import SFTTrainer
|
| 5 |
+
from transformers import TrainingArguments
|
| 6 |
+
from datasets import load_dataset
|
| 7 |
+
|
| 8 |
+
# --- 設定項目 ---
|
| 9 |
+
|
| 10 |
+
# 1. ベースモデルの指定
|
| 11 |
+
# UnslothはLlama, Mistral, CodeLlama, TinyLlama, Vicuna, DPOなどをサポートしています。
|
| 12 |
+
# ここでは、以前の議論に基づき `openai/gpt-oss-20b` に相当する強力なオープンモデルとして
|
| 13 |
+
# `meta-llama/Llama-3-8B-Instruct` を使用します。
|
| 14 |
+
# Unslothは、このモデルの読み込みを自動で最適化してくれます。
|
| 15 |
+
model_name = "unsloth/llama-3-8b-Instruct-bnb-4bit" # Unslothが提供する4bit量子化済みモデルを使うとメモリ効率が非常に良い
|
| 16 |
+
|
| 17 |
+
# 2. データセットファイル
|
| 18 |
+
dataset_file = "dataset_2000.jsonl"
|
| 19 |
+
|
| 20 |
+
# 3. ファインチューニング後のモデル名(Hugging Face Hubに公開する名前)
|
| 21 |
+
hub_model_name = "kofdai/Mindspark-GPT-OSS-20B-Ilm" # 必ず "your-hf-username" をあなたのHFユーザー名に書き換えてください
|
| 22 |
+
|
| 23 |
+
# --- メイン処理 ---
|
| 24 |
+
|
| 25 |
+
def main():
|
| 26 |
+
# Unslothによる高速なモデル読み込み
|
| 27 |
+
# 4bit量子化を有効にし、データ型を自動選択(MacのMPSではbfloat16がサポートされていれば使われる)
|
| 28 |
+
model, tokenizer = FastLanguageModel.from_pretrained(
|
| 29 |
+
model_name = model_name,
|
| 30 |
+
max_seq_length = 2048,
|
| 31 |
+
dtype = None, # Noneで自動選択
|
| 32 |
+
load_in_4bit = True,
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
# LoRAモデルの設定
|
| 36 |
+
model = FastLanguageModel.get_peft_model(
|
| 37 |
+
model,
|
| 38 |
+
r = 16, # LoRAランク
|
| 39 |
+
target_modules = ["q_proj", "k_proj", "v_proj", "o_proj",
|
| 40 |
+
"gate_proj", "up_proj", "down_proj"],
|
| 41 |
+
lora_alpha = 16,
|
| 42 |
+
lora_dropout = 0,
|
| 43 |
+
bias = "none",
|
| 44 |
+
use_gradient_checkpointing = True,
|
| 45 |
+
random_state = 3407,
|
| 46 |
+
use_rslora = False,
|
| 47 |
+
loftq_config = None,
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
# データセットの準備
|
| 51 |
+
dataset = load_dataset("json", data_files=dataset_file, split="train")
|
| 52 |
+
|
| 53 |
+
# トレーニングの実行
|
| 54 |
+
trainer = SFTTrainer(
|
| 55 |
+
model = model,
|
| 56 |
+
tokenizer = tokenizer,
|
| 57 |
+
train_dataset = dataset,
|
| 58 |
+
dataset_text_field = "text", # Unslothではこの形式が一般的
|
| 59 |
+
max_seq_length = 2048,
|
| 60 |
+
dataset_num_proc = 2,
|
| 61 |
+
packing = False, # Falseを推奨
|
| 62 |
+
formatting_func = lambda example: f"### 指示:\n{example['prompt']}\n\n### 応答:\n{example['response']}",
|
| 63 |
+
args = TrainingArguments(
|
| 64 |
+
per_device_train_batch_size = 2,
|
| 65 |
+
gradient_accumulation_steps = 4,
|
| 66 |
+
warmup_steps = 5,
|
| 67 |
+
num_train_epochs = 3,
|
| 68 |
+
learning_rate = 2e-4,
|
| 69 |
+
fp16 = not torch.backends.mps.is_available(), # MPSではFalse, CUDAではTrue
|
| 70 |
+
bf16 = torch.backends.mps.is_available(), # MPSではTrue
|
| 71 |
+
logging_steps = 1,
|
| 72 |
+
optim = "adamw_8bit",
|
| 73 |
+
weight_decay = 0.01,
|
| 74 |
+
lr_scheduler_type = "linear",
|
| 75 |
+
seed = 3407,
|
| 76 |
+
output_dir = "outputs",
|
| 77 |
+
),
|
| 78 |
+
)
|
| 79 |
+
|
| 80 |
+
print("Unslothを使ったファインチューニングを開始します...")
|
| 81 |
+
trainer.train()
|
| 82 |
+
print("ファインチューニングが完了しました。")
|
| 83 |
+
|
| 84 |
+
# モデルの保存 (Hugging Face Hubへのアップロード)
|
| 85 |
+
print("モデルをHugging Face Hubにアップロードします...")
|
| 86 |
+
# Hugging Faceにログインしていることを確認してください (huggingface-cli login)
|
| 87 |
+
model.push_to_hub(hub_model_name, use_auth_token = True)
|
| 88 |
+
tokenizer.push_to_hub(hub_model_name, use_auth_token = True)
|
| 89 |
+
print(f"モデルを {hub_model_name} としてHubに公開しました。")
|
| 90 |
+
|
| 91 |
+
if __name__ == "__main__":
|
| 92 |
+
main()
|
lps.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import faiss
|
| 2 |
+
import numpy as np
|
| 3 |
+
from sentence_transformers import SentenceTransformer
|
| 4 |
+
import os
|
| 5 |
+
import pickle
|
| 6 |
+
|
| 7 |
+
class LatentPatternStore:
|
| 8 |
+
"""「薄い霧」として機能する潜在パターンストア(LPS)。
|
| 9 |
+
テキストの意味をベクトル化し、類似度に基づいた検索を行う。
|
| 10 |
+
"""
|
| 11 |
+
def __init__(self, model_name='all-MiniLM-L6-v2', store_path='./lps_data'):
|
| 12 |
+
print("LPSを初期化中...")
|
| 13 |
+
self.model = SentenceTransformer(model_name)
|
| 14 |
+
self.dimension = self.model.get_sentence_embedding_dimension()
|
| 15 |
+
self.store_path = store_path
|
| 16 |
+
self.index_file = os.path.join(store_path, 'lps.index')
|
| 17 |
+
self.mapping_file = os.path.join(store_path, 'lps_mapping.pkl')
|
| 18 |
+
|
| 19 |
+
if os.path.exists(self.index_file):
|
| 20 |
+
self.load_store()
|
| 21 |
+
else:
|
| 22 |
+
self.index = faiss.IndexFlatL2(self.dimension)
|
| 23 |
+
self.entry_mapping = []
|
| 24 |
+
os.makedirs(self.store_path, exist_ok=True)
|
| 25 |
+
print(f"LPSの初期化完了。現在 {len(self.entry_mapping)} 件の記憶を保持しています。")
|
| 26 |
+
|
| 27 |
+
def add_entries(self, texts: list[str]):
|
| 28 |
+
"""新しい記憶(テキスト)をLPSに追加する。"""
|
| 29 |
+
print(f"{len(texts)}件の新しい記憶を追加中...")
|
| 30 |
+
vectors = self.model.encode(texts, convert_to_tensor=False)
|
| 31 |
+
self.index.add(np.array(vectors).astype('float32'))
|
| 32 |
+
self.entry_mapping.extend(texts)
|
| 33 |
+
print("追加完了。")
|
| 34 |
+
|
| 35 |
+
def search(self, query: str, k: int = 3) -> list[str]:
|
| 36 |
+
"""クエリ(トリガー)に意味的に近い記憶をk件検索する。"""
|
| 37 |
+
if not self.entry_mapping:
|
| 38 |
+
return []
|
| 39 |
+
|
| 40 |
+
query_vector = self.model.encode([query])
|
| 41 |
+
distances, indices = self.index.search(np.array(query_vector).astype('float32'), k)
|
| 42 |
+
|
| 43 |
+
# 見つかったインデックスに対応する元のテキストを返す
|
| 44 |
+
results = [self.entry_mapping[i] for i in indices[0] if i != -1]
|
| 45 |
+
return results
|
| 46 |
+
|
| 47 |
+
def save_store(self):
|
| 48 |
+
"""現在のLPSの状態(インデックスとマッピング)をファイルに保存する。"""
|
| 49 |
+
print("LPSの状態を保存中...")
|
| 50 |
+
faiss.write_index(self.index, self.index_file)
|
| 51 |
+
with open(self.mapping_file, 'wb') as f:
|
| 52 |
+
pickle.dump(self.entry_mapping, f)
|
| 53 |
+
print(f"保存完了。場所: {self.store_path}")
|
| 54 |
+
|
| 55 |
+
def load_store(self):
|
| 56 |
+
"""ファイルからLPSの状態を読み込む。"""
|
| 57 |
+
print(f"既存のLPSの状態を {self.store_path} から読み込み中...")
|
| 58 |
+
self.index = faiss.read_index(self.index_file)
|
| 59 |
+
with open(self.mapping_file, 'rb') as f:
|
| 60 |
+
self.entry_mapping = pickle.load(f)
|
| 61 |
+
print("読み込み完了。")
|
| 62 |
+
|
| 63 |
+
# テスト用の簡易的な実行
|
| 64 |
+
if __name__ == '__main__':
|
| 65 |
+
# LPSインスタンスを作成(./lps_data にデータがなければ新規作成、あれば読み込む)
|
| 66 |
+
lps = LatentPatternStore()
|
| 67 |
+
|
| 68 |
+
# LPSが空の場合のみ、初期データを追加
|
| 69 |
+
if lps.index.ntotal == 0:
|
| 70 |
+
print("LPSが空のため、初期データを追加します。")
|
| 71 |
+
initial_memories = [
|
| 72 |
+
"LLMのファインチューニング",
|
| 73 |
+
"UIデザインの原則",
|
| 74 |
+
"刹那的な記憶を持つAIの思想",
|
| 75 |
+
"階層型記憶システム",
|
| 76 |
+
"ユーザーの思考パターンを反映する",
|
| 77 |
+
"mindsparkというキーワード",
|
| 78 |
+
"煩わしいという感情表現"
|
| 79 |
+
]
|
| 80 |
+
lps.add_entries(initial_memories)
|
| 81 |
+
lps.save_store()
|
| 82 |
+
|
| 83 |
+
print("\n--- 検索テスト ---")
|
| 84 |
+
query1 = "新しいユーザーインターフェースのアイデア" # UIデザインに関連
|
| 85 |
+
results1 = lps.search(query1, k=2)
|
| 86 |
+
print(f"クエリ: '{query1}'")
|
| 87 |
+
print(f"-> 検索結果: {results1}")
|
| 88 |
+
|
| 89 |
+
query2 = "AIの記憶方法について" # 刹那的記憶、階層型記憶に関連
|
| 90 |
+
results2 = lps.search(query2, k=2)
|
| 91 |
+
print(f"\nクエリ: '{query2}'")
|
| 92 |
+
print(f"-> 検索結果: {results2}")
|
split_data.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import random
|
| 3 |
+
|
| 4 |
+
# --- 設定 ---
|
| 5 |
+
input_file = 'train.jsonl'
|
| 6 |
+
output_train_file = 'train.jsonl' # 元のファイルを上書き
|
| 7 |
+
output_valid_file = 'valid.jsonl'
|
| 8 |
+
valid_split_ratio = 0.1 # 10%を検証データにする
|
| 9 |
+
# -------------
|
| 10 |
+
|
| 11 |
+
print(f"'{input_file}' を読み込んでいます...")
|
| 12 |
+
|
| 13 |
+
# 元のデータを読み込む
|
| 14 |
+
with open(input_file, 'r', encoding='utf-8') as f:
|
| 15 |
+
lines = f.readlines()
|
| 16 |
+
|
| 17 |
+
# データをシャッフルしてランダム性を確保
|
| 18 |
+
random.shuffle(lines)
|
| 19 |
+
|
| 20 |
+
# 分割点を計算
|
| 21 |
+
split_index = int(len(lines) * (1 - valid_split_ratio))
|
| 22 |
+
|
| 23 |
+
# 学習データと検証データに分割
|
| 24 |
+
train_data = lines[:split_index]
|
| 25 |
+
valid_data = lines[split_index:]
|
| 26 |
+
|
| 27 |
+
# 新しい学習データとして書き出す
|
| 28 |
+
with open(output_train_file, 'w', encoding='utf-8') as f:
|
| 29 |
+
f.writelines(train_data)
|
| 30 |
+
print(f"学習データを '{output_train_file}' に保存しました。({len(train_data)}行)")
|
| 31 |
+
|
| 32 |
+
# 検証データとして書き出す
|
| 33 |
+
with open(output_valid_file, 'w', encoding='utf-8') as f:
|
| 34 |
+
f.writelines(valid_data)
|
| 35 |
+
print(f"検証データを '{output_valid_file}' に保存しました。({len(valid_data)}行)")
|
| 36 |
+
|
| 37 |
+
print("分割が完了しました。")
|
| 38 |
+
|
update_readme.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from huggingface_hub import HfApi
|
| 2 |
+
|
| 3 |
+
api = HfApi()
|
| 4 |
+
repo_id = "kofdai/gemma3-27b-mindspark"
|
| 5 |
+
local_file_path = "ilm-upload-package/README.md"
|
| 6 |
+
repo_file_path = "README.md" # リポジトリ内のパス
|
| 7 |
+
|
| 8 |
+
try:
|
| 9 |
+
api.upload_file(
|
| 10 |
+
path_or_fileobj=local_file_path,
|
| 11 |
+
path_in_repo=repo_file_path,
|
| 12 |
+
repo_id=repo_id,
|
| 13 |
+
repo_type="model",
|
| 14 |
+
)
|
| 15 |
+
print(f"Successfully updated {repo_file_path} in {repo_id}")
|
| 16 |
+
except Exception as e:
|
| 17 |
+
print(f"Error updating {repo_file_path}: {e}")
|