Yoshimaru's Blog

京都に住む院生が書いています

【AI利用者向け】SVGを貼ると即プレビュー&ダウンロードできるChrome拡張

なぜ作ったのか

研究用の図を作ることが多く,その中でSVGのアイコン使う場面が多いです. 最近の生成AIの高性能化のおかげで,目的のSVGをコードとして一気に作ることができるようになったのですが,そのあとが少し面倒でした.

生成されたSVGを一度VSCodeに貼り付けて, ブラウザで開いて、見た目を確認して、また戻って修正して…… という往復を毎回やるのが地味に手間.

なので、貼った瞬間にプレビューできて、そのまま保存までできるChrome拡張を作りました.

インストールはこちら

chromewebstore.google.com

できること

かなりシンプルに設計.

  • SVGコードを貼る

  • その場でプレビューされる

  • 問題なければ .svg としてダウンロードできる

余計な機能は入れていません.(データももちろん送っていません)

使い方

Gemini等,生成AIとかでSVGを作っている際に,

テキストエリアにSVGコードをそのまま貼る。 ダウンロードできる

  • プレビューを見ながら調整
    • コードを少し直すと、即座にプレビューが更新される。

コード

github.com

是非使ってください.

DEIM2025 記録

DEIM2025(福岡)に行った際の記録を残しておきます。

 

オンライン(1-3日目)

今年も直立ハイブリットでした。DB系の基盤技術の研究を聞ける大会は日本でここくらいじゃないですかね?とても勉強になります。

ZOOMのロビー機能が便利でした。毎回zoomの開き直しがなくブラウザ内で色々見て回れるので、最後の方はロビーだけで研究を聞いていました。

 

オンサイト(4,5日目)

対面学会は福岡国際会議場で行われました。f:id:maruruy:20250317023300j:image
f:id:maruruy:20250317023255j:image

なかなか広い会場で、今年は会場前に豪華な看板もあり、手が混んでるなと言う印象。ただ、交通面はバスのみで、周りも繁華街ではないのでお昼は悩みどころでした。


BoFは企画担当しました。90分の司会はなかなか大変(次やるなら半分半分くらいにした方がいいかも、、、)

あと部屋の外もあった軽食はあんまり食べれなかったのは残念。

 

f:id:maruruy:20250317024024j:image

クロージングの部屋は狭いかなと思っていたけど、実は部屋の外に中継されていたので、談笑しながら参加ができる利点があるなと思いました。(大きなホールだと静かにしておかないといけないので。。。)

 

おわりに

知り合いも増えてきたので来年も参加すると思います。開催地は未定みたいですが、神戸になると予想してみます。

 

以降写真

f:id:maruruy:20250317023605j:image
f:id:maruruy:20250317023602j:image
f:id:maruruy:20250317023609j:image

DBSJ学生企画の人たちに連れられて、有名店長野へ。水炊き初めて食べました

 

f:id:maruruy:20250317023519j:image

もつ鍋うますぎた
f:id:maruruy:20250317023515j:image

明太子もうますぎた

 

Pytorch Geometricで知識グラフを処理には?(ナレッジグラフ Advent Calendar 2024)

ナレッジグラフ Advent Calendar 2024の記事です. https://adventar.org/calendars/11112

最近,GNN実装でPytorch Geometricとにらめっこしていたので,ナレッジグラフとリンク予測とかGNNとかを実装するときにPytorch Geometric(以降PyG)をどう使えるのかをふと思ったので,軽くまとめてみます.


グラフと一般的なKGとの違い

そもそも,GNNで全部処理できるんじゃないかということですが,一般的なグラフ処理と知識グラフ処理の自分が見ている中でのイメージは,以下のようなものです.

  • グラフ → 頂点と辺(ノード; Node)と辺(エッジ; Edge)で構成.無向グラフ有向グラフ重み付きグラフなど様々
  • 知識グラフ →同様にノードとエッジで構成される.そして,エンティティ間のリレーションに自然言語の属性がある.(head, relation, tail で構成)

グラフのイメージ(引用:https://distill.pub/2021/gnn-intro/

KGのイメージ(引用:https://medium.com/neo4j/enhancing-the-accuracy-of-rag-applications-with-knowledge-graphs-ad5e2ffab663

このことををまとめると「グラフ」とすると,ノードとエッジで構築された構造を意味するのに対して,「知識グラフ」とすると何かしらで表現された(テキスト)知識がグラフ上に組み入っているイメージです.


Pytorch Geometricとは

Pytorch GeometricはPytorchベースで書かれたGNN用のライブラリです. Pytorchの深層学習の書き方に倣っており,いろいろな研究のGNNが実装されていて, 他にもStellarGraphというものもあるのですが,こちらはTensorFlowベースで,書き方はかなり楽なのですが,中身のカスタマイズが難しいと思います.(GNN系の国際会議論文で公開されているコードはPyGでの実装が多い印象です.

普通のPytorchの例
class SimpleNN(nn.Module):
    def __init__(self, input_dim, hidden_dim, output_dim):
        super(SimpleNN, self).__init__()
        # 1層目: 全結合層
        self.fc1 = nn.Linear(input_dim, hidden_dim)
        # 2層目: 全結合層
        self.fc2 = nn.Linear(hidden_dim, output_dim)

    def forward(self, x):
        # 活性化関数ReLUを適用
        x = F.relu(self.fc1(x))
        # 出力層
        x = self.fc2(x)
        return x
Pytorch Geometric の例
class GCN(nn.Module):
    def __init__(self, input_dim, hidden_dim, output_dim):
        super(GCN, self).__init__()
        # 1層目: グラフ畳み込み層 (GCNConv)
        self.conv1 = GCNConv(input_dim, hidden_dim)
        # 2層目: グラフ畳み込み層 (GCNConv)
        self.conv2 = GCNConv(hidden_dim, output_dim)

    def forward(self, x, edge_index):
        # 活性化関数ReLUを適用
        x = F.relu(self.conv1(x, edge_index))
        # 出力層
        x = self.conv2(x, edge_index)
        return x

調べてみた

知識グラフ埋め込み (Knowleage Graph Embedding; KGE)

KGEに関しては,KGE Modelsとして公式実装されています. - KGEModel: KGEのための基底クラス.下の論文実装から継承されている.

KGEは最新のものでRotatE(ICLR19)くらいしかまだ追えていないイメージ.(GNN系は最新モデルもインプリされている印象です)決定的なKGEモデルが考案されていないからかもしれないです.

公式での明確な言及はなさそうですがこんな事例はありました.ここでは推薦における異種グラフのLPとして実装していますね. ここでは新たにClassifireモジュールを導入しているようです.

class Classifier(torch.nn.Module):
    def forward(self, x_user: Tensor, x_movie: Tensor, edge_label_index: Tensor) -> Tensor:
        edge_feat_user = x_user[edge_label_index[0]]
        edge_feat_movie = x_movie[edge_label_index[1]]
        return (edge_feat_user * edge_feat_movie).sum(dim=-1)

こちらもKGEのようにそれぞれのnode embeddingを作って,それを線形変換などいろいろしてクラス分類(リンクがあるか無いかを分類)しているようです.

最後に

今回はKGとPyGでできることをKGEとLPを対象に軽く調べました. 自分でもさらに調べたら更新していきますので,使う際の参考になれば幸いです.

スクレイピング・クローリングに関する疑問に対する根拠まとめ

自分自身スクレイピングやクローリングで研究したりするのですが,その際にいつも思いつく疑問とそれに対する答え・根拠をまとめます.

スクレイピングする際は自己責任でお願いします.


そもそもスクレイピングは合法?

スクレイピングしたものの著作権は?

  • もちろんWebサイト運営側に帰属.なので著作権保護法の全てが適応される.*2

Webサイトにスクレイピング禁止と書かれている場合は?

  • これはダメ.Webページの利用規約違反になる.そもそもWebページの利用規約はアカウント停止や損害賠償請求が課せれる場合がある.*3

リクエストの間隔は1秒あければよい?

  • これには明確な根拠なし.Librahack事件のとき,1秒間隔でリクエストしていて,これはスクレイピングする人にとっては常識の範囲だったが,サーバの事情で負荷がかかった模様.どんな工夫をしたとしても,向こうがDDOS攻撃だと思ったらこっちが悪くなる.(← おそらくスクレイピングする人が考えるべきことはここだと思う.)

スクレイピングしたデータの解析,ブログや論文などで公開していい?

  • いわゆる二次創作物になる.つまり一次創作(スクレイピングしたWebサイト)を題材に作った創作物にあたる.そして二次創作の著作権に関してはかなりグレー.許可がないと基本ダメ*4

スクレイピングしたデータの解析をもとにしたサービスを提供して金銭を得ていい?

  • 二次創作物で金銭を得る行為なので同人誌関連を見るべきか.*5

結論

商用利用しない場合,ここが肝になるかと.

リクエストの間隔は1秒あければよい?

スクレイピングしようがその解析結果を論文で公表しようが,その際のスクレイピングDDOS攻撃だと認識されてしまえばアウト.こちらがどんだけ気を遣ってアクセスしようが,向こうで実害が起きたら(というか実害だと判断されたら)攻撃されたことになる.あと,いざ責任問題になった時にネットワークの所在とかになるので,所属機関(大学や会社)ではないところからしたほうがいいのでは?

なのでスクレイピングする時に我々にできることは,

  • Webサイトの利用規約に明確にスクレイピング禁止と書いているかを確認
  • 最大限の努力でサーバに負荷がかからないようにコードを書く
  • ネットワークは所属機関に迷惑がかからないように,自宅や自分名義で契約しているネットワークを使用
  • 相手のサーバで異変が起こらないように祈る

くらいかなと思います.

日本食品標準成分表を本来あるべき姿に戻す

日本食品標準成分表の前処理

このサイトに公開されている 日本食品標準成分表はPDFもしくはExcel 形式で公開しているのですが,分析者はもちろんPDFを選ぶわけなくExcelにせざるおえないのですが,これがまぁ大変.

大変な点1:列結合でバラバラ - エクセルなので列結合とかをして汚すぎる,Pandasで入れるにもちょっとややこしい.

大変な点2:食材名に第分類とか小分類がごちゃ混ぜで表記.ここに一応その分類の定義について記述されているが,それが結合している状態.(生データも配ってよ)

成分表のエクセル,とても分析すぐできない

コードは以下のリポジトリにあります.

github.com

ちなみに力技で各ファイル1つ1つ見ていってどの行までが列数か備考はどこかなどを見ていきました.

出力

2つのファイルが得られます.

food_categories_all.csv - 食材名を本来の姿に戻す. 列名は「食品番号,類区分,副分類,大分類,中分類,小分類,細分1,細分2,細分3,細分4」

食品番号 類区分 副分類 大分類 中分類 小分類 細分1 細分2 細分3 細分4
1001 アマランサス 玄穀
1002 あわ 精白粒
1003 あわ あわもち
1003 あわ あわもち
1004 えんばく オートミール
1004 えんばく オートミール
1005 おおむぎ 七分つき押麦
1005 おおむぎ 七分つき押麦
1006 おおむぎ 押麦

food_categories_all.csv - 栄養素を本来の姿に戻す 列名は「food_group,food_code,reference_number,food_name,REFUSE,ENERC,(以降栄養素が続く)」

food_group food_code reference_number food_name REFUSE ENERC ENERC_KCAL WATER PROTCAA_x PROT-_x ...
1 1001 0001 アマランサス 玄穀 0.0 1452.0 343.0 13.5 (11.3) 12.7 ...
1 1002 0002 あわ 精白粒 0.0 1466.0 346.0 13.3 10.2 11.2 ...
1 1003 0003 あわ あわもち 48.0 ...
1 1003 0003 あわ あわもち 0.0 890.0 210.0 48.0 (4.5) 5.1 ...
1 1004 0004 えんばく オートミール 10.0 ...
1 1004 0004 えんばく オートミール 0.0 1479.0 350.0 10.0 12.2 13.7 ...
1 1005 0005 おおむぎ 七分つき押麦 14.0 ...

栄養素

今は成分識別子のわかりずらい記号なのでいずれ対応表作ります.(PubChemとか化学系との連携もいずれします)

Slackで締切リマインドのコマンドを生成するアプリを作りました

Slackの締切の日時を基準に,リマインドコマンドを生成するアプリを作りました.

動機

何かの締め切りがあった時にSlackのリマンダーを作るかと思うのですが,日時が決まっているものに対して1週間前,2日前など設定したい時があった. ただ,日時を個別に指定するのが大変なのでそこを自動化してくれるアプリをつくりました.

ページ

以下のページで公開しています.(デザインが手抜きなのは許しでください...)

blooming-falls-71077-b4f7ce20c543.herokuapp.com

Github上にも公開しています.

github.com

機能

アプリは以下の図のようなものです.

アプリ画面

  • メッセージを入力: slackのリマインダーに送りたいメッセージを書きます.(〇〇の締切など)このとき,channelをメンションするかどうかも決めれます(チェックを入れると冒頭に@channelが入力されます)

  • 締切を入力: その締切の日時を入力,時刻も入力します.

  • いつリマインドするかを生成: そのリマインダーをいつリマインドさせたいのかを複数選択します.

  • 生成&コピー: 生成ボタンを作るとコマンドが作成されるのでコピーボタンを押してSlackへ投稿します.

実行すると以下のようなコマンドが生成されます.

/remind "@channel 〇〇の締切" 2024-06-21 at 09:00
/remind "@channel 〇〇の締切" 2024-06-20 at 09:00
/remind "@channel 〇〇の締切" 2024-06-19 at 09:00

これをslackのチャンネルに投稿すれば動作するかと思います.(1行ごとに投稿しないといけないので,1行ごとにコピーできるようにしています.)

備考

  • 環境:Python(Flusk),html,CSS
  • デプロイ:Heroku
  • 何かあればgithub issueでお願いします🙏

Google Tasksのタスクを自動的にSlackに投稿する

きっかけ

皆さんは毎日のタスク管理ってどうしてますか?

自分は研究室で毎日のタスクを報告する習慣があるのですが,その際ついつい忘れたり,書くことで満足しちゃってることがありました. なのでTodoサービスにあるタスクを毎日読み込んで自動でSlackに投稿する機能を作ってみました.

動作例は以下です. このようにたくさんタスクがあった時に.

このようなタスクがあるとして,

このようにSlackの特定のチャンネルにタスクリスト,タスク,サブタスクが綺麗に表示できます

動作例

自分はTodo系のアプリは長続きしなかったのですが,自動投稿されて誰かに見られるかも...とおもうと続く気がします..(まだ初めてまもないですが) Google TasksはPCだとなぜかブラウザ版がなかったりするので,以下のクライアントアプリが便利,無料 https://thetodo.net/ja/

thetodo.net

準備

使うのは以下.

  • Slack Webhook
  • App Script
  • Google SpreadSheet

流れとしては

  1. Google Tasksからその日のタスク一覧をGoogle SpreadSheet に記入する
  2. そのGoogle SpretSheetからApp Scriptで投稿テキストを生成する
  3. 生成されたテキストをSlack Webhookを通じて投稿する

以上の機能をApp Scriptのトリガー機能で毎朝や毎週などに設定して使えます.

1. Google Tasks -> Google SpreadSheet

SpreadSheetで以下のカラム名を1行目に作ります.

| リスト | タスク名 | メインサブ | 親タスク | 詳細 | 期限 | 完了・未完了 |

その後に画像にある拡張機能からApp Scriptを探す.

スプレットシートの前準備

サービスからTasksを追加しておく.

App Scriptには以下のコードを添付する.

function formatDueDate(dateString) {
  var date = new Date(dateString);
  var formattedDate = date.getFullYear() + '/' + (date.getMonth() + 1) + '/' + date.getDate();
    return formattedDate;
}

function syncTasksToSheet() {
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Sheet1');
    // スプレッドシートにデータが1行だけの場合は、以下の処理をスキップ
  if (sheet.getLastRow() > 1) {
    sheet.getRange(2, 1, sheet.getLastRow() - 1, sheet.getLastColumn()).clear();
  }
  var taskLists = Tasks.Tasklists.list();
  var parentTaskMap = {}; // 親タスクのIDと名前をマッピングするための辞書

  if (taskLists.items) {
    taskLists.items.forEach(function(taskList) {
      var tasks = Tasks.Tasks.list(taskList.id).items;
      if (tasks) {
        // 先に全てのメインタスクをマップに登録
        tasks.forEach(function(task) {
          if (!task.parent) {
            parentTaskMap[task.id] = task.title;
          }
        });

        // タスクの処理
        tasks.forEach(function(task) {
          var isSubtask = task.parent ? true : false;
          var parentTaskName = isSubtask ? parentTaskMap[task.parent] : 'main';
          var row = [
            taskList.title, // リスト名
            task.title, // タスク名
            isSubtask ? 'サブ' : 'メイン', // メインタスクかサブタスクか
            isSubtask ? parentTaskName : 'main', // 親タスク
            task.notes || '', // 詳細
            task.due ? formatDueDate(task.due) : '', // 期限
            task.status === 'completed' ? '完了' : '未完了', // 完了・未完了
          ];
          Logger.log(row);
          sheet.appendRow(row);
        });
      }
    });
  }
}

コードの処理としては, - まず最初にスプレットシートのデータを削除する.(2回目以降で)

syncTasksToSheet関数を実行して以下のようになっていたらOK

またさっき作ったSpreadSheetにも自動で追加されてると思います.

2. App Scriptで投稿文生成

今スプレットシートにタスク一覧が手に入ったので,そこからSlackに投稿する文を生成します. 以下の App Scriptは例でカスタマイズの余地があると思います

function postToSlackmain() {
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  var data = sheet.getDataRange().getValues();
  var tasks = {};
  var formattedText = '';

  data.forEach(function(row, index) {
    if (index === 0) return; // ヘッダー行をスキップ

    var [list, task, mainSub, parentTask, detail, deadline, status] = row;

    if (mainSub === 'メイン') {
      if (!tasks[list]) {
        tasks[list] = {};
      }
      tasks[list][task] = {
        'detail': detail,
        'deadline': deadline ? Utilities.formatDate(new Date(deadline), Session.getScriptTimeZone(), 'yyyy/MM/dd') : '期限なし',
        'subtasks': tasks[list][task] ? tasks[list][task]['subtasks'] : [] // 既存のサブタスクを保持
      };
    } else if (mainSub === 'サブ') {
      if (!tasks[list]) {
        tasks[list] = {};
      }
      if (!tasks[list][parentTask]) {
        tasks[list][parentTask] = { 'subtasks': [] }; // 仮のメインタスクを作成
      }
      tasks[list][parentTask]['subtasks'].push({
        'name': task,
        'status': status,
        'detail': detail,
        'deadline': deadline ? Utilities.formatDate(new Date(deadline), Session.getScriptTimeZone(), 'yyyy/MM/dd') : '期限なし',
      });
    }
  });
  var formattedText = '';
  // フォーマットされたテキストを生成
  for (var listName in tasks) {
    formattedText += `====== 【${listName}】 ============================================= \n`;
    for (var taskName in tasks[listName]) {
      var task = tasks[listName][taskName];
      formattedText += `*${taskName}* (締切:${task.deadline}) \n`;
      if (task.detail) {
        formattedText += `>${task.detail.replace(/\n/g, '\n>').replace(/\r/g, '\n>')} \n`;
      }
      task.subtasks.forEach(function(subtask) {
        formattedText += `   • ${subtask.name} :${subtask.status}: (締切:${subtask.deadline}) \n`;
      });
    }
  }
  postToSlack(formattedText);
}

function postToSlack(message) {
  var webhookUrl = '******************************************'; // ここにSlackのWebhook URLを設定
  var payload = {
    'text': message // 送信するメッセージ
  };

  var options = {
    'method': 'post',
    'contentType': 'application/json',
    'payload': JSON.stringify(payload)
  };

  UrlFetchApp.fetch(webhookUrl, options);
}

機能としてはスプレットシートからいい感じに文を生成して,PostToSlack関数に投げます. ここでWebhookURLが必要ですが,3で手に入れます.

その後この関数を毎日(もしは毎週など)実行されるようにトリガーを設定する. この時注意して欲しいのが, syncTasksToSheet→postToSlackmain の順で実行できるように時間をずらさないと最新の情報を取れないです.

トリガー設定

3. Slack Webhookを有効にして投稿

以下のURLからWebhookを作る. https://dumil.slack.com/apps/new/A0F7XDUAZ--incoming-webhook-

参考サイト https://documents.trocco.io/docs/how-to-generate-slack-webhook-url

投稿するチャンネルを指定して,Incoming Webhookを作成する.

Webhookの設定,アイコンや説明などはここで追加する

これで,2で指定したトリガーをもとに

追加機能

自分は追加で日毎に差分を調査して,完了したタスクを毎日投稿する設定にしてます. その場合のコードは以下

function getSpreadsheetData() {
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Sheet1');
  var range = sheet.getDataRange();
  var values = range.getValues();
  var tasks = [];

  for (var i = 1; i < values.length; i++) {
    var row = values[i];
    tasks.push({ title: row[1], status: row[6] }); // タスク名と状態(完了・未完了)を取得
  }
  return tasks;
}

function notifyCompletedTasks() {
  var spreadsheetTasks = getSpreadsheetData();
  var taskLists = Tasks.Tasklists.list();
  var message = "今日完了したタスク \n";
  if (taskLists.items) {
    taskLists.items.forEach(function(taskList) {
      var tasks = Tasks.Tasks.list(taskList.id).items;
      var parentTaskMap = {};

      // 全てのメインタスクをマップに登録
      tasks.forEach(function(task) {
        if (!task.parent) {
          parentTaskMap[task.id] = task.title;
        }
      });
      
      // タスクの処理
      tasks.forEach(function(task) {
        if (task.status === 'completed') {
          var isSubtask = task.parent ? true : false;
          var parentTaskName = isSubtask ? parentTaskMap[task.parent] : '';
          var spreadsheetTask = spreadsheetTasks.find(t => t.title === task.title);

          // スプレッドシートにないが完了した新たなタスク、または状態が変わったタスクを特定
          if (!spreadsheetTask || (spreadsheetTask && spreadsheetTask.status !== '完了')) {
            message += isSubtask ? ` • ${task.title}(メインタスク:${parentTaskName}\n` : task.title;
            
          }
        }
      });
      
    });
  }
  postToSlack(message);
}

これを同じように毎日トリガーを設定すれば以下のようにその日の完了タスクを投稿してくれます.

またここにある完了だったり未完了というのはスタンプを作っているので適応できています.

このサイトで簡単に作れるので便利

emoji-gen.ninja