0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

RTX 5060 Ti (Blackwell) + PyTorchでつくるITニュース自動分類・DB蓄積システム

0
Posted at

はじめに

日々のITニュース(RSS)を自動で収集・分類し、ダッシュボードで閲覧できるシステムを構築しました。

本記事では、最新のNVIDIA GeForce RTX 5060 Ti (Blackwellアーキテクチャ / sm_120) 環境においてPyTorchでファインチューニングを行い、PostgreSQLへ効率的にUpsert保存、さらにFastAPI + systemdでWebサービスとして常駐化させるまでの一連の手順とハマりどころの解決法をまとめます。


システム構成

  • OS: Ubuntu Linux 24.04 LTS
  • GPU: NVIDIA GeForce RTX 5060 Ti (Compute Capability: sm_120)
  • Python: 3.12 (venv)
  • Frameworks: PyTorch (Nightly cu128), Hugging Face Transformers (cl-tohoku/bert-base-japanese), FastAPI, SQLAlchemy / psycopg2
  • DB: PostgreSQL 16 (itnews_db)

1. RTX 5060 Ti (sm_120) 環境での PyTorch セットアップ

RTX 5000シリーズ(Blackwellアーキテクチャ)で既存のPyTorch(CUDA 12.1/12.4等)を動かすと、以下のようなカーネル未対応エラーが発生することがあります。

RuntimeError: CUDA error: no kernel image is available for execution on the device

これを回避するため、CUDA 12.8以降に対応した Nightlyビルドの PyTorch を導入します。

# 仮想環境内でのインストール
python -m pip install --pre torch torchvision torchaudio --index-url [https://download.pytorch.org/whl/nightly/cu128](https://download.pytorch.org/whl/nightly/cu128)

GPU認識の確認

import torch
print('CUDA Available:', torch.cuda.is_available())
print('Device Name:', torch.cuda.get_device_name(0) if torch.cuda.is_available() else 'None')

CUDA Available: True, Device Name: NVIDIA GeForce RTX 5060 Ti が表示されれば準備完了です。


2. PostgreSQL への Upsert パイプライン (predict_rss.py)

推論したカテゴリと確信度(Confidence)をデータベースへ直接一括挿入・更新(Upsert)します。URL (link) を重複チェックキーとして ON CONFLICT 構文を使用します。

import os
import torch
import torch.nn.functional as F
import pandas as pd
from transformers import AutoTokenizer, AutoModelForSequenceClassification
from sqlalchemy import create_engine, text
from sqlalchemy.engine import URL

MODEL_DIR = "./saved_model"
INPUT_CSV = "./data/latest_rss_news.csv"

# DB接続設定
db_url = URL.create(
    drivername="postgresql+psycopg2",
    username="postgres",
    password="your_password",
    host="localhost",
    port=5432,
    database="itnews_db",
)

CATEGORY_MAP = {
    0: "OpenSource/OSS", 1: "AI/Data", 2: "Enterprise/Cloud",
    3: "Software/Dev", 4: "Hardware/PC", 5: "Mobile/Gadget",
    6: "Security", 7: "Network/Telecom", 8: "Business/Tech"
}

def main():
    df = pd.read_csv(INPUT_CSV)
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

    tokenizer = AutoTokenizer.from_pretrained(MODEL_DIR)
    model = AutoModelForSequenceClassification.from_pretrained(MODEL_DIR).to(device)
    model.eval()

    titles = df["title"].fillna("").astype(str).tolist()
    full_texts = df["full_text"].fillna("").astype(str).tolist()
    texts = [f"{t} {ft}".strip() for t, ft in zip(titles, full_texts)]

    # バッチ推論
    batch_size = 32
    predicted_cat_ids, predicted_cats, confidences = [], [], []

    for i in range(0, len(texts), batch_size):
        batch_texts = texts[i:i+batch_size]
        inputs = tokenizer(batch_texts, max_length=256, padding=True, truncation=True, return_tensors="pt").to(device)
        with torch.no_grad():
            outputs = model(**inputs)
            probs = F.softmax(outputs.logits, dim=-1)
            max_probs, preds = torch.max(probs, dim=-1)
            for pred_id, prob in zip(preds.cpu().numpy(), max_probs.cpu().numpy()):
                predicted_cat_ids.append(int(pred_id))
                predicted_cats.append(CATEGORY_MAP.get(int(pred_id), "Business/Tech"))
                confidences.append(round(float(prob), 4))

    df["category_id"] = predicted_cat_ids
    df["predicted_category"] = predicted_cats
    df["confidence"] = confidences

    # DBへの保存 (Upsert)
    engine = create_engine(db_url, connect_args={"client_encoding": "utf8"})
    
    upsert_sql = text("""
        INSERT INTO news (title, summary, full_text, link, published, published_date, source, category_id, category_name, confidence)
        VALUES (:title, :summary, :full_text, :link, :published, :published_date, :source, :category_id, :category_name, :confidence)
        ON CONFLICT (link) DO UPDATE SET
            title = EXCLUDED.title,
            category_id = EXCLUDED.category_id,
            category_name = EXCLUDED.category_name,
            confidence = EXCLUDED.confidence;
    """)

    with engine.begin() as conn:
        for _, row in df.iterrows():
            conn.execute(upsert_sql, {
                "title": str(row.get("title", "")),
                "summary": str(row.get("summary", "")),
                "full_text": str(row.get("full_text", "")),
                "link": str(row.get("link", "")),
                "published": pd.to_datetime(row.get("published"), errors="coerce"),
                "published_date": str(row.get("published_date", "")),
                "source": str(row.get("source", "不明")),
                "category_id": int(row.get("category_id", 8)),
                "category_name": str(row.get("predicted_category", "Business/Tech")),
                "confidence": float(row.get("confidence", 0.0))
            })

if __name__ == "__main__":
    main()

3. BERTモデルの再学習 (train.py)

Transformers v5系の仕様変更(evaluation_strategy $\rightarrow$ eval_strategy 等)に対応した学習スクリプトです。FP16(混合精度学習)を有効化し、RTX 5060 Ti の性能をフル活用します。

import torch
import pandas as pd
from sqlalchemy import create_engine
from sqlalchemy.engine import URL
from sklearn.model_selection import train_test_split
from transformers import AutoTokenizer, AutoModelForSequenceClassification, Trainer, TrainingArguments
from datasets import Dataset

# DBから学習用データをロード
engine = create_engine(db_url, connect_args={"client_encoding": "utf8"})
df = pd.read_sql("SELECT title, summary, full_text, category_id FROM news WHERE category_id IS NOT NULL", engine)
df["text"] = df["title"] + " " + df["summary"].fillna("") + " " + df["full_text"].fillna("")
df["label"] = df["category_id"].astype(int)

train_df, val_df = train_test_split(df[["text", "label"]], test_size=0.2, random_state=42)

tokenizer = AutoTokenizer.from_pretrained("cl-tohoku/bert-base-japanese")
def tokenize_fn(examples):
    return tokenizer(examples["text"], padding="max_length", truncation=True, max_length=256)

train_ds = Dataset.from_pandas(train_df).map(tokenize_fn, batched=True)
val_ds = Dataset.from_pandas(val_df).map(tokenize_fn, batched=True)

model = AutoModelForSequenceClassification.from_pretrained("cl-tohoku/bert-base-japanese", num_labels=9)

use_gpu = torch.cuda.is_available()

training_args = TrainingArguments(
    output_dir="./results",
    num_train_epochs=3,
    per_device_train_batch_size=16 if use_gpu else 8,
    fp16=use_gpu,          # GPU使用時のみ混合精度学習を有効化
    eval_strategy="epoch",  # transformers 5.x 準拠
    save_strategy="epoch",
    learning_rate=2e-5,
    logging_strategy="steps",
    load_best_model_at_end=True,
    report_to="none"
)

trainer = Trainer(model=model, args=training_args, train_dataset=train_ds, eval_dataset=val_ds)
trainer.train()

model.save_pretrained("./saved_model")
tokenizer.save_pretrained("./saved_model")

4. systemd による Web サーバー常駐化

FastAPI (app.py) を Linux サービスとしてバックグラウンドで自動起動・常駐化させます。

サービスファイルの作成

/etc/systemd/system/itnews-web.service

[Unit]
Description=IT News FastAPI Web Server & Dashboard
After=network.target postgresql.service

[Service]
Type=simple
User=yyoshida
WorkingDirectory=/home/yyoshida/workspace/gputest
ExecStart=/home/yyoshida/workspace/gputest/venv/bin/uvicorn app:app --host 0.0.0.0 --port 8000
Restart=always

[Install]
WantedBy=multi-user.target

サービスの有効化と起動

sudo systemctl daemon-reload
sudo systemctl enable itnews-web.service
sudo systemctl start itnews-web.service

# ステータス確認
sudo systemctl status itnews-web.service

おわりに

最新世代のGPU環境ではライブラリのバージョン整合性(特にPyTorch NightlyとCUDA toolkit)が重要ですが、環境を揃えることで約1,800件の推論・分類が数秒、再学習もわずか30秒足らずで完了する非常に快適なMLパイプラインが構築できました。

定期実行バッチ(cron/タスクスケジューラ)と組み合わせることで、運用すればするほど精度が向上するITニュースダッシュボードとして活用できます。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?