Yoshimaru's Blog

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

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

gpt-4-vision-preview APIを試してみる

2023/11/7のOpenAI Developer Day keynoteでgpt-4Turbo・GPTsなどが取り沙汰されていたが,前から話題だった画像理解のAPIもきていたので使ってみた.

以下が公式URL platform.openai.com

コード

画像URLを指定して,その表示+説明文生成です.ColabなりJupyter想定です.

import openai
import os
import requests
from IPython.display import display, Image
from io import BytesIO

os.environ['OPENAI_API_KEY'] ="*******" #<- ここにAPIのキーを入力

image_url = "https://cdn.pixabay.com/photo/2018/04/26/16/31/marine-3352341_1280.jpg"#例のURL


from openai import OpenAI

client = OpenAI()

response = client.chat.completions.create(
    model="gpt-4-vision-preview",
    messages=[
        {
            "role": "user",
            "content": [
                {"type": "text", "text": "次に入力される画像の説明を日本語で行ってください"},
                {
                    "type": "image_url",
                    "image_url": image_url,
                },
            ],
        }
    ],
    max_tokens=300,
)
res_txt = response.choices[0].message.content


response = requests.get(image_url)
response.raise_for_status()  # ステータスコードが200でない場合は例外を発生させる
display_width = 500  # 例として幅を100ピクセルに設定

# 取得したバイトデータをIPython.display.Imageオブジェクトに変換し、サイズを指定して表示
image = Image(response.content, width=display_width)

# 画像を表示
display(image)
# 説明文を改行区切りで表示
for sentence in res_txt.split('。'):
    if sentence:
        print(sentence.strip() + '。')

実行結果

実行結果
"role": "user"とかをみると普通のGPT 4などと同じように会話履歴だったり初期プロンプトなども入れれそう.あと公式の例は英語だったけど普通に日本語も対応している.

以下のColabファイルで試せます.

colab.research.google.com

備考

ちなみにですが,料金はGPT4 Turboと同じような気がします.であればかなりリーズナブル

OpenAIのモデル表

情報系院生に向けた研究インターン体験記まとめ

情報系院生に向けた研究インターン体験記まとめ

個人的にまとめていたインターンの体験談を載せます. 対象としている企業は,

を基準にしています.参考になればと思います. (就活〇〇サイトなどは省いて,個人の記事のみを扱っています.) 随時更新します.

NTT研究所

www.ntt-labs.jp

Microsoft Research Asia

www.microsoft.com

OMRON SINIC X

www.omron.com

サイバーエージェント「AI Lab」

www.cyberagent.co.jp

更新日

2023年10月

書き出す時に欠損値がないのに,読み込んだら欠損値が出てしまう場合

主にPandasを利用時

エラー状況 - 保存する時にはnanがない 例えば以下で集計

df.isnull().sum()

このときは全て0になる

ただ読み込んで同じコードを書くと数が増える.

おそらくの原因

文字列 nan が含まれている!

そのときは以下のようにread_csvの引数を指定して読み込む.

pd.read_csv("パス名", keep_default_na=False)

備考

(ちなみに自分はレシピデータを扱うことが多いのですが,カレーに付けるいわゆる「ナン」の英語表記が原因でした,,,食メディア研究あるあると言いたい)

締め切り計算に便利なNotion関数

説明

Notionのデータベースに締め切りを入れると,後何日か自動で計算してくれる関数を作りました

準備

ステータスも用意しておく.(今回の場合In progressになっているところだけ表示される)

ステータスカラム
新たに関数のカラムを追加

スクリプト

関数の数式のところに以下を入力

if(prop("ステータス") == "In progress", if(dateBetween(prop("締切"), now(), "days") + 1 > 0, if(empty(prop("締切")), "", "期限まで" + format(dateBetween(prop("締切"), now(), "days") + 1) + "日"), ""), "")

prop("締切")のところは各自で設定している締め切りのカラム名にしてください.

機能

  • 締め切りまで何日かがわかる
  • ステータスが"In progress"になっているところだけが表示される
  • 過ぎた締切は何も表示されない

実際の例

Pytorchで独自の損失関数を設計したときにgrad_fnが無いと言われた

背景

既存モデルのLossをマルチラベルに対応できるように改良していたときに以下で詰まっていた.

lement 0 of tensors does not require grad and does not have a grad_fn

やりたいこととしては多クラス分類→多ラベル分類. そのときに元々あったLossであるnn.CrossEntropyLoss をシグモイド出力からのnn.BCEWithLogitsLoss()に変えたかった.

原因

おそらくlossの値を逆伝播するときにgrad_fnを参考に行っていると考えられる. 独自の平均だったり合計などをlistとかに変えて計算しまうと,これらの情報が消えているのでおかしくなるのでは?

grad_fnはどんな計算があってそのテンソルができたかを残しておくもので,例えば平均によって計算されたら以下になる.

tensor(0.7343, device='cuda:0', grad_fn=<MeanBackward0>)

解決

torch.stackでlistを変換してmeanを適応した.

rtn = torch.stack(losses).mean()

ちなみにlossesにはBCEWithLogitsLoss()で計算されたlossの値が入っている配列.

[tensor(0.5282, device='cuda:0',
       grad_fn=<BinaryCrossEntropyWithLogitsBackward0>), tensor(0.9063, device='cuda:0',
       grad_fn=<BinaryCrossEntropyWithLogitsBackward0>), tensor(0.7409, device='cuda:0',
       grad_fn=<BinaryCrossEntropyWithLogitsBackward0>), tensor(0.6982, device='cuda:0',
       ...

また,結果をtensor型で新たに定義して引数requires_grad=Trueを設定(以下コード)することでもできるが,ここまで保持してた他情報(例えばGPUのdeviceとか)がなくなるので面倒.

torch.tensor(losses,requires_grad=True)

まず擬似的なマルチラベルと最終層で計算されたベクトルを作る.

import random 
label_mtx = torch.tensor([[random.choice([0,1]) for i in range(6)] for j in range(20)], dtype=torch.float32,device=device,requires_grad=True)
score_mtx = torch.tensor([[random.random() for i in range(6)] for j in range(20)], dtype=torch.float32,device=device,requires_grad=True)

そしてLossを計算する関数を定義. LossはBCEWithLogitsLoss(), (ちなみにBCEWithLogitsLossは内部でシグモイドを適応してくれるみたいなのでモデルの出力をそのまま入れて良いらしい)

def mtx_BCEWithLogitsLoss(score_mtx,label_mtx):
    loss_fn = nn.BCEWithLogitsLoss()
    losses = []
    for score_i,label_i in zip(score_mtx,label_mtx):
        loss_i = loss_fn(score_i,label_i)
        losses.append(loss_i)

    rtn = torch.stack(losses).mean()
    return rtn

実行すると以下.

mtx_BCEWithLogitsLoss(score_mtx,label_mtx)
>>>tensor(0.7343, device='cuda:0', grad_fn=<MeanBackward0>)

ちゃんとtensorに情報を持っています.

参考にしたもの

Kaggleにあった画像マルチラベルしているやつ www.kaggle.com

もし違う場合など指摘があればコメントして欲しいです.

対話ロボットコンペティション2022を終えて

こんにちは,

今回は対話ロボットコンペティション2022(DRC2022)に参加した感想などをまとめていきたいと思います.

(おそらくあるであろう)DRC2023に参加する予定の方など興味のある方の参考になればと思います. (※ 私が参加したチームの内容や内情はありませんのご了承ください)

DRC2022とは

コンペティションは,文部科学省科学研究費助成事業「新学術領域研究」 人間機械共生社会を目指した対話知能システム学(対話知能学)のプロジェクトの一環として行われます sites.google.com

新研究領域を発展させるための一つの試みみたいです. 今回は2回目で前回はオンラインが主で行われたみたいですが,コロナの状況が収まってきたおかげか,予選本選ともに対面で行われていました.

スケジュール感

2022年のおおよそのスケジュールについてまとめておきます

  • 4月初旬:申し込み
  • 8月まで:説明会・情報共有会(2・3回)
  • 7月中旬から8月初旬まで:リモートテスト(数回)
  • 8月中旬から下旬まで:予選会(東京・日本科学未来館
  • 9月中旬:予選結果発表
  • 9月末ごろ:Paper Descriptionの提出
  • 10月末:ArXivへの投稿,本選(IROS2022・京都国際会館

予選

東京にある日本科学未来館の自由に入れるスペースで行われました. 夏休み期間に行われたせいか,結構な人が体験してくれてより正当な評価が行えたんじゃないでしょうか. (ちなみに本選で選ばれるかどうかはこの予選での体験者アンケートで決まりました)

予選の様子:区切られているブースの奥にロボットがありました

Paper Description

予選に出場した各チームは,開発したシステムについての論文を執筆しました. コンペ運営側でプロシーディングスとしてまとめていて,以下URLで見ることができます.

arxiv.org

本選

本線は10/24 -10/26に京都国際会館で行われていたIROS2022の併設ワークショップとして開催されました. そこでは本選に出場したチームのシステムがお偉いさん方の体験があり,プレゼンもありました. その点数を総合的に評価され1位が決まりました.

本選が行われたIROS2022
IROSのレジストレーション
本選出場したチームのデモとプレゼン

感想

自分の研究分野ではあまりデモという形で行うことが少なく,プレゼン発表しにいくという感じなので新鮮でよかったです.実装できなかった部分もあるので,ぜひまた来年も出たいと思いました. また結構注目されているコンペではと感じました.結構動画も上がっています.ぜひご覧ください!

www.youtube.com

www.youtube.com