簡易的なＲＡＧ検討

前に作ったＲＡＧだが、少しアルゴリズムを変える検討をしている。
結果的にはＴＦＩＤＦでガッツリやらなくても、ＮＧＲＡＭ、バイグラムで
そこそこできることがわかった

## 三つの前処理方式

| 手法       | アプローチ                     | 語彙サイズ | 実行時間 | 強み                              | 適用例                              |
|------------|--------------------------------|-----------|----------|-----------------------------------|-------------------------------------|
| 松| Janome形態素解析              | 66（最小） | 0.011秒（若干重い） | 高い検索精度、意味理解            | 意味重視の検索システム、専門クエリ  |
| 竹 | 文字保持+N-gram               | 380（最大）| 0.003秒（高速）    | 情報量とスピードのバランス        | 一般検索、多様な表現のシステム       |
| 梅  | 文字除去+バイグラム           | 183       | 0.003秒（高速）    | 高速、シンプル                    | リアルタイム処理、速度重視のシステム |
### 主要引用
- [Janomeのドキュメント](https://janome.mocobeta.dev/ja/)
- [N-gramの解説](https://en.wikipedia.org/wiki/N-gram)
- [Word n-gram language modelの解説](https://en.wikipedia.org/wiki/Word_n-gram_language_model)
- [バイグラムの例](https://kavita-ganesan.com/what-are-n-grams/)

## 精度比較

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212914/9926341e-9ff4-4e5b-954d-72bf32b17c09.png)


```
================================================================================
前処理手法の詳細分析
================================================================================
元テキスト: Python機械学習入門！scikit-learnライブラリを使った分析。

■ HTML版前処理
クリーニング後: Python機械学習入門scikitlearnライブラリを使った分析
トークン化後: 機械 械学 学習 習入 入門 門ラ ライ イブ ブラ ラリ リを を使 使っ った た分 分析        
トークン数: 16

■ Flask版前処理（Janome形態素解析）
クリーニング後: Python機械学習入門！scikitlearnライブラリを使った分析。
トークン化後: python 機械 学習 入門 ! scikitlearn ライブラリ 使っ 分析
トークン数: 9

■ Flask版前処理（形態素解析なし）
クリーニング後: Python機械学習入門！scikitlearnライブラリを使った分析。
トークン化後: 機械 械学 学習 習入 入門 門ラ ライ イブ ブラ ラリ リを を使 使っ った た分 分析 機械学 
械学習 学習入 習入門 入門ラ 門ライ ライブ イブラ ブラリ ラリを リを使 を使っ 使った った分 た分析    
トークン数: 31

================================================================================
前処理手法別性能比較
================================================================================
対象文書数: 8
検索クエリ数: 4

【検索 1】クエリ: 'Python機械学習'
------------------------------------------------------------

■ HTML版（文字除去+バイグラム）
前処理時間: 0.000秒
TF-IDF計算時間: 0.003秒
総実行時間: 0.003秒
語彙サイズ: 183語
ヒット件数: 2件
上位結果:
  1. 類似度: 0.3590 | Python機械学習入門講座。scikit-learnライブラリを使用してデータ...
  2. 類似度: 0.0882 | AI画像認識技術の基礎。TensorFlowとPyTorchを使った深層学習の実...

■ Flask版（文字保持+N-gram）
前処理時間: 0.000秒
TF-IDF計算時間: 0.004秒
総実行時間: 0.004秒
語彙サイズ: 380語
ヒット件数: 2件
上位結果:
  1. 類似度: 0.3023 | Python機械学習入門講座。scikit-learnライブラリを使用してデータ...
  2. 類似度: 0.0470 | AI画像認識技術の基礎。TensorFlowとPyTorchを使った深層学習の実...

■ Flask版（形態素解析）
前処理時間: 0.015秒
TF-IDF計算時間: 0.002秒
総実行時間: 0.017秒
語彙サイズ: 66語
ヒット件数: 3件
上位結果:
  1. 類似度: 0.5553 | Python機械学習入門講座。scikit-learnライブラリを使用してデータ...
  2. 類似度: 0.1454 | データ可視化とグラフ作成。Pythonのmatplotlibとseabornライ...
  3. 類似度: 0.1233 | AI画像認識技術の基礎。TensorFlowとPyTorchを使った深層学習の実...

================================================================================

【検索 2】クエリ: 'React開発！'
------------------------------------------------------------

■ HTML版（文字除去+バイグラム）
前処理時間: 0.001秒
TF-IDF計算時間: 0.002秒
総実行時間: 0.003秒
語彙サイズ: 183語
ヒット件数: 2件
上位結果:
  1. 類似度: 0.3220 | JavaScript開発環境の構築。Node.jsとnpmを使ったモダンな開発ワ...
  2. 類似度: 0.1598 | Web開発フレームワーク比較！ReactとVue.jsの特徴について詳しく解説。...

■ Flask版（文字保持+N-gram）
前処理時間: 0.000秒
TF-IDF計算時間: 0.003秒
総実行時間: 0.003秒
語彙サイズ: 380語
ヒット件数: 2件
上位結果:
  1. 類似度: 0.2249 | JavaScript開発環境の構築。Node.jsとnpmを使ったモダンな開発ワ...
  2. 類似度: 0.1130 | Web開発フレームワーク比較！ReactとVue.jsの特徴について詳しく解説。...

■ Flask版（形態素解析）
前処理時間: 0.007秒
TF-IDF計算時間: 0.003秒
総実行時間: 0.010秒
語彙サイズ: 66語
ヒット件数: 2件
上位結果:
  1. 類似度: 0.3652 | Web開発フレームワーク比較！ReactとVue.jsの特徴について詳しく解説。...
  2. 類似度: 0.2904 | JavaScript開発環境の構築。Node.jsとnpmを使ったモダンな開発ワ...

================================================================================

【検索 3】クエリ: 'データベース最適化'
------------------------------------------------------------

■ HTML版（文字除去+バイグラム）
前処理時間: 0.000秒
TF-IDF計算時間: 0.003秒
総実行時間: 0.003秒
語彙サイズ: 183語
ヒット件数: 3件
上位結果:
  1. 類似度: 0.4326 | データベース最適化手法の実践。SQLクエリのパフォーマンス改善テクニック。...     
  2. 類似度: 0.0851 | データ可視化とグラフ作成。Pythonのmatplotlibとseabornライ...
  3. 類似度: 0.0732 | Python機械学習入門講座。scikit-learnライブラリを使用してデータ...

■ Flask版（文字保持+N-gram）
前処理時間: 0.000秒
TF-IDF計算時間: 0.003秒
総実行時間: 0.003秒
語彙サイズ: 380語
ヒット件数: 3件
上位結果:
  1. 類似度: 0.4223 | データベース最適化手法の実践。SQLクエリのパフォーマンス改善テクニック。...     
  2. 類似度: 0.0648 | データ可視化とグラフ作成。Pythonのmatplotlibとseabornライ...
  3. 類似度: 0.0562 | Python機械学習入門講座。scikit-learnライブラリを使用してデータ...

■ Flask版（形態素解析）
前処理時間: 0.007秒
TF-IDF計算時間: 0.002秒
総実行時間: 0.009秒
語彙サイズ: 66語
ヒット件数: 1件
上位結果:
  1. 類似度: 0.4115 | データベース最適化手法の実践。SQLクエリのパフォーマンス改善テクニック。...     

================================================================================

【検索 4】クエリ: 'AWS構築'
------------------------------------------------------------

■ HTML版（文字除去+バイグラム）
前処理時間: 0.000秒
TF-IDF計算時間: 0.002秒
総実行時間: 0.002秒
語彙サイズ: 183語
ヒット件数: 2件
上位結果:
  1. 類似度: 0.1590 | JavaScript開発環境の構築。Node.jsとnpmを使ったモダンな開発ワ...
  2. 類似度: 0.1314 | クラウドインフラ構築ガイド。AWSとAzureを活用したスケーラブルなシステム設...    

■ Flask版（文字保持+N-gram）
前処理時間: 0.001秒
TF-IDF計算時間: 0.002秒
総実行時間: 0.003秒
語彙サイズ: 380語
ヒット件数: 2件
上位結果:
  1. 類似度: 0.1118 | JavaScript開発環境の構築。Node.jsとnpmを使ったモダンな開発ワ...
  2. 類似度: 0.0924 | クラウドインフラ構築ガイド。AWSとAzureを活用したスケーラブルなシステム設...    

■ Flask版（形態素解析）
前処理時間: 0.007秒
TF-IDF計算時間: 0.002秒
総実行時間: 0.009秒
語彙サイズ: 66語
ヒット件数: 2件
上位結果:
  1. 類似度: 0.3965 | クラウドインフラ構築ガイド。AWSとAzureを活用したスケーラブルなシステム設...    
  2. 類似度: 0.1419 | JavaScript開発環境の構築。Node.jsとnpmを使ったモダンな開発ワ...

================================================================================

📊 総合分析
================================================================================
平均実行時間:
  HTML版（文字除去+バイグラム）: 0.003秒
  Flask版（文字保持+N-gram）: 0.003秒
  Flask版（形態素解析）: 0.011秒

平均語彙サイズ:
  HTML版（文字除去+バイグラム）: 183語
  Flask版（文字保持+N-gram）: 380語
  Flask版（形態素解析）: 66語

平均ヒット件数:
  HTML版（文字除去+バイグラム）: 2.2件
  Flask版（文字保持+N-gram）: 2.2件
  Flask版（形態素解析）: 2.0件

🔍 結論:
==================================================
形態素解析の効果:
  実行時間差: +309.1% (形態素解析版)
  語彙サイズ差: -63.9% (形態素解析版)

文字除去の違いの影響:
  語彙サイズ差: +107.7% (Flask文字保持版)
```



```python
import tkinter as tk
from tkinter import ttk, scrolledtext, filedialog, messagebox
import re
import time
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity

# Janome関連のインポート（利用可能な場合のみ）
try:
    from janome.tokenizer import Tokenizer
    from janome.analyzer import Analyzer
    from janome.charfilter import UnicodeNormalizeCharFilter
    from janome.tokenfilter import POSKeepFilter, POSStopFilter, LowerCaseFilter
    JANOME_AVAILABLE = True
    tokenizer = Tokenizer()
except ImportError:
    JANOME_AVAILABLE = False

class TextSimilarityGUI:
    def __init__(self, root):
        self.root = root
        self.root.title("文書類似度検索システム (アルゴリズム比較対応)")
        self.root.geometry("900x700")
        
        # データ保持用
        self.documents = []
        self.processed_docs = []
        self.vectorizer = None
        
        self.create_widgets()
        self.load_demo_data()

    def create_widgets(self):
        # ファイル選択部分
        file_frame = ttk.LabelFrame(self.root, text="ファイル選択", padding=10)
        file_frame.pack(fill="x", padx=10, pady=5)
        
        self.file_path = tk.StringVar()
        ttk.Entry(file_frame, textvariable=self.file_path, width=50).pack(side="left", padx=5)
        ttk.Button(file_frame, text="参照", command=self.browse_file).pack(side="left", padx=5)
        ttk.Button(file_frame, text="デモデータ", command=self.load_demo_data).pack(side="left", padx=5)

        # アルゴリズム選択部分
        algorithm_frame = ttk.LabelFrame(self.root, text="前処理アルゴリズム選択", padding=10)
        algorithm_frame.pack(fill="x", padx=10, pady=5)
        
        self.algorithm_var = tk.StringVar(value="html")
        
        # ラジオボタン配置
        ttk.Radiobutton(algorithm_frame, text="HTML版 (文字除去+バイグラム)", 
                       variable=self.algorithm_var, value="html").pack(side="left", padx=10)
        ttk.Radiobutton(algorithm_frame, text="Flask版 (文字保持+N-gram)", 
                       variable=self.algorithm_var, value="flask_no_janome").pack(side="left", padx=10)
        
        # Janome利用可能な場合のみ有効化
        janome_radio = ttk.Radiobutton(algorithm_frame, text="Flask版 (形態素解析)", 
                                      variable=self.algorithm_var, value="flask_janome")
        janome_radio.pack(side="left", padx=10)
        if not JANOME_AVAILABLE:
            janome_radio.configure(state="disabled")

        # 検索部分
        search_frame = ttk.LabelFrame(self.root, text="検索", padding=10)
        search_frame.pack(fill="x", padx=10, pady=5)
        
        self.query_entry = ttk.Entry(search_frame, width=50)
        self.query_entry.pack(side="left", padx=5)
        ttk.Button(search_frame, text="検索", command=self.search).pack(side="left", padx=5)
        
        # 件数設定
        self.top_n = tk.StringVar(value="5")
        ttk.Label(search_frame, text="表示件数:").pack(side="left", padx=5)
        ttk.Entry(search_frame, textvariable=self.top_n, width=5).pack(side="left")

        # 結果表示部分
        result_frame = ttk.LabelFrame(self.root, text="検索結果", padding=10)
        result_frame.pack(fill="both", expand=True, padx=10, pady=5)

        # ツリービュー
        columns = ("rank", "similarity", "text", "algorithm")
        self.tree = ttk.Treeview(result_frame, columns=columns, show="headings")
        
        # 列の設定
        self.tree.heading("rank", text="順位")
        self.tree.heading("similarity", text="類似度")
        self.tree.heading("text", text="内容")
        self.tree.heading("algorithm", text="アルゴリズム")
        
        self.tree.column("rank", width=50)
        self.tree.column("similarity", width=100)
        self.tree.column("text", width=500)
        self.tree.column("algorithm", width=150)

        # スクロールバー
        scrollbar = ttk.Scrollbar(result_frame, orient="vertical", command=self.tree.yview)
        self.tree.configure(yscrollcommand=scrollbar.set)

        # 配置
        self.tree.pack(side="left", fill="both", expand=True)
        scrollbar.pack(side="right", fill="y")

        # ステータスバー
        self.status_var = tk.StringVar()
        self.status_bar = ttk.Label(self.root, textvariable=self.status_var, relief="sunken")
        self.status_bar.pack(fill="x", padx=10, pady=5)
        self.status_var.set("準備完了")

    def browse_file(self):
        file_path = filedialog.askopenfilename(
            filetypes=[("テキストファイル", "*.txt"), ("CSVファイル", "*.csv"), ("すべてのファイル", "*.*")]
        )
        if file_path:
            self.file_path.set(file_path)
            self.load_file(file_path)

    def load_file(self, file_path):
        try:
            with open(file_path, 'r', encoding='utf-8') as f:
                self.documents = [line.strip() for line in f if line.strip()]
            self.process_documents()
            self.status_var.set(f"ファイル読み込み完了: {len(self.documents)}行")
        except Exception as e:
            messagebox.showerror("エラー", f"ファイル読み込みエラー: {str(e)}")

    def load_demo_data(self):
        self.documents = [
            "Python機械学習入門講座。scikit-learnライブラリを使用してデータ分析を学習します。",
            "Web開発フレームワーク比較！ReactとVue.jsの特徴について詳しく解説。",
            "データベース最適化手法の実践。SQLクエリのパフォーマンス改善テクニック。",
            "クラウドインフラ構築ガイド。AWSとAzureを活用したスケーラブルなシステム設計です。",
            "AI画像認識技術の基礎。TensorFlowとPyTorchを使った深層学習の実装方法。"
        ]
        self.process_documents()
        self.status_var.set("デモデータ読み込み完了")
        self.file_path.set("[デモデータ]")

    # ========== 旧版の前処理メソッド ==========
    def preprocess_text(self, text):
        # 文字除去（記号など）
        cleaned = re.sub(r'[^\w\sぁ-んァ-ヶヷ-ヺー一-龥]', '', text)
        return cleaned.strip()

    def tokenize_text(self, text):
        clean = self.preprocess_text(text)
        tokens = []
        
        # 英単語抽出
        english_words = re.findall(r'\b[a-zA-Z]+\b', clean)
        tokens.extend([w.lower() for w in english_words])
        
        # 日本語バイグラム
        japanese = re.sub(r'[a-zA-Z0-9\s]', '', clean)
        for i in range(len(japanese) - 1):
            tokens.append(japanese[i:i+2])
        
        return ' '.join(tokens)

    # ========== HTML版の前処理（文字除去のみ） ==========
    def preprocess_html_style(self, text):
        """HTML版の前処理：正規表現による文字除去のみ"""
        cleaned = re.sub(r'[^\w\sぁ-んァ-ヶヷ-ヺー一-龥]', '', text)
        return cleaned.strip()
    
    def tokenize_html_style(self, texts):
        """HTML版のトークン化（バイグラム）"""
        processed_texts = []
        for text in texts:
            clean = self.preprocess_html_style(text)
            
            # 英単語抽出
            tokens = []
            english_words = re.findall(r'\b[a-zA-Z]+\b', clean)
            tokens.extend([w.lower() for w in english_words])
            
            # 日本語バイグラム
            japanese = re.sub(r'[a-zA-Z0-9\s]', '', clean)
            for i in range(len(japanese) - 1):
                tokens.append(japanese[i:i+2])
            
            processed_texts.append(' '.join(tokens))
        
        return processed_texts
    
    # ========== Flask版の前処理（現在の実装） ==========
    def preprocess_flask_style(self, text):
        """Flask版の前処理：より多くの文字を保持"""
        cleaned = re.sub(r'[^a-zA-Z0-9ぁ-んァ-ヶヷ-ヺー一-龥、。！？\s.,!?]', '', text)
        return cleaned
    
    def tokenize_flask_with_janome(self, texts):
        """Flask版のJanome形態素解析"""
        if not JANOME_AVAILABLE:
            return self.tokenize_flask_without_janome(texts)
        
        char_filters = [UnicodeNormalizeCharFilter()]
        token_filters = [
            POSKeepFilter(['名詞', '動詞', '形容詞']),
            POSStopFilter(['助詞', '助動詞']),
            LowerCaseFilter(),
        ]
        
        analyzer = Analyzer(char_filters=char_filters, tokenizer=tokenizer, token_filters=token_filters)
        
        processed_texts = []
        for text in texts:
            preprocessed = self.preprocess_flask_style(text)
            tokens = [token.surface for token in analyzer.analyze(preprocessed)]
            processed_texts.append(' '.join(tokens))
        
        return processed_texts
    
    def tokenize_flask_without_janome(self, texts):
        """Flask版だが形態素解析なし（フォールバック）"""
        processed_texts = []
        for text in texts:
            clean = self.preprocess_flask_style(text)
            
            # 英単語抽出
            tokens = []
            english_words = re.findall(r'\b[a-zA-Z]+\b', clean)
            tokens.extend([w.lower() for w in english_words])
            
            # 日本語部分はより長いN-gram
            japanese = re.sub(r'[a-zA-Z0-9\s.,!?、。！？]', '', clean)
            for n in range(2, 4):  # 2-3gram
                for i in range(len(japanese) - n + 1):
                    tokens.append(japanese[i:i+n])
            
            processed_texts.append(' '.join(tokens))
        
        return processed_texts

    def get_algorithm_name(self, algorithm_key):
        """アルゴリズムキーから表示名を取得"""
        names = {
            'html': 'HTML版',
            'flask_no_janome': 'Flask版(N-gram)',
            'flask_janome': 'Flask版(形態素解析)'
        }
        return names.get(algorithm_key, algorithm_key)

    def process_documents(self):
        """選択されたアルゴリズムで文書を処理"""
        algorithm = self.algorithm_var.get()
        
        if algorithm == 'html':
            self.processed_docs = self.tokenize_html_style(self.documents)
        elif algorithm == 'flask_janome':
            self.processed_docs = self.tokenize_flask_with_janome(self.documents)
        elif algorithm == 'flask_no_janome':
            self.processed_docs = self.tokenize_flask_without_janome(self.documents)
        else:
            # デフォルト（後方互換性）
            self.processed_docs = [self.tokenize_text(doc) for doc in self.documents]
        
        # TF-IDF準備
        self.vectorizer = TfidfVectorizer()
        self.vectorizer.fit(self.processed_docs)
        
        algorithm_name = self.get_algorithm_name(algorithm)
        self.status_var.set(f"文書処理完了 ({algorithm_name}): 語彙数 {len(self.vectorizer.vocabulary_)}")

    def search(self):
        query = self.query_entry.get().strip()
        if not query:
            messagebox.showwarning("警告", "検索語を入力してください")
            return
            
        if not self.documents:
            messagebox.showwarning("警告", "文書が読み込まれていません")
            return

        # 結果クリア
        for item in self.tree.get_children():
            self.tree.delete(item)

        # 選択されたアルゴリズムでクエリ処理
        algorithm = self.algorithm_var.get()
        start_time = time.time()
        
        if algorithm == 'html':
            processed_query = self.tokenize_html_style([query])[0]
        elif algorithm == 'flask_janome':
            processed_query = self.tokenize_flask_with_janome([query])[0]
        elif algorithm == 'flask_no_janome':
            processed_query = self.tokenize_flask_without_janome([query])[0]
        else:
            processed_query = self.tokenize_text(query)
        
        # TF-IDF変換
        query_vec = self.vectorizer.transform([processed_query])
        doc_matrix = self.vectorizer.transform(self.processed_docs)
        
        # 類似度計算
        similarities = cosine_similarity(query_vec, doc_matrix).flatten()
        
        processing_time = time.time() - start_time
        
        # 結果作成
        results = []
        for i, sim in enumerate(similarities):
            if sim > 0:
                results.append((i, sim, self.documents[i]))
        
        # ソート
        results.sort(key=lambda x: x[1], reverse=True)
        
        # 上位N件表示
        try:
            top_n = int(self.top_n.get())
        except ValueError:
            top_n = 5

        algorithm_name = self.get_algorithm_name(algorithm)
        for rank, (idx, sim, text) in enumerate(results[:top_n], 1):
            self.tree.insert("", "end", values=(rank, f"{sim:.4f}", text, algorithm_name))

        self.status_var.set(f"検索完了 ({algorithm_name}): {len(results)} 件中 {min(top_n, len(results))} 件を表示 (処理時間: {processing_time:.3f}秒)")

def main():
    root = tk.Tk()
    app = TextSimilarityGUI(root)
    root.mainloop()

if __name__ == "__main__":
    main()

```