배경
AID 동아리에는 부원들이 사용하는 디스코드 서버가 있다. AID 서버에는 [채용 공고] 채널이 존재하는데 부원들이 알게된 AI/인턴 채용 공고 정보를 서로 공유하는 공간이다. 이를 Github Actions와 Discord Hooks을 활용해 자동화하기로 했다.
상세 구현 사항은 다음과 같다.
- 채용 공고 크롤링
- 최근 게시물 인덱스 관리
- AID 채널로 공고 전송: Discord Hooks
- 매일 특정 시간마다 스케줄링: Github Actions
- AI 기반 채용 공고 필터링: Transformers.js
개발 환경
- Bun
- Github Actions
- Discord Hooks
채용 공고 크롤링 Job 구현
이 부분은 그냥 크롤링해오면 되는 거라 어려울 건 없다. 가장 익숙한 Nodejs를 사용해서 구현했다. 새 버전이 출시된 Bun을 찍먹해보기 위해 사용해봤는데, TypeScript 실행을 지원해서 빌드할 필요가 없다는 점과 실행 시간이 엄청 빠르다는 점이 무척 사용성이 좋았다.
Discord Hooks
디스코드 훅을 이용해서 채널에 메시지를 보내야 한다. 사실 이 부분도 자료가 워낙 많아서 어려울 것 없다. 해당 채널의 웹훅키를 발급받고, 해당 url에 POST 요청을 보내면 된다. 메시지를 이쁘게 하기 위해 간단한 템플릿을 적용했다.
import axios from "axios";
import { ContentType } from "../type/content";
export function convertDataWithTemplate(data: ContentType[]) {
let content = "[오늘의 채용 공고]\n\n";
for (const item of data) {
content += `공고: ${item.contentLabel}\n링크: <${item.contentUrl}>\n\n`;
}
content += "";
return content;
}
export async function sendDiscordNotification(message: string) {
try {
const WEBHOOK_URL = process.env.AID_DISCORD_WEBHOOK_URL;
if (!WEBHOOK_URL) {
throw new Error("webhook url is nothing!!");
}
const res = await axios.post(WEBHOOK_URL, {
content: message,
embed: [
{
title: "오늘의 채용 공고",
description: message,
color: 0x00ff00,
timestamp: new Date(),
fields: [
{
name: "필드 1 제목",
value: "필드 1 내용",
inline: false,
},
{
name: "필드 12 제목",
value: "필드 12 내용",
inline: false,
},
],
footer: {
text: "KimCookieYa",
},
},
],
});
// console.log("result:", res);
return true;
} catch (error) {
console.error(error);
return false;
}
}
Github Actions
Github Actions은 자동화의 핵심이다. 매일 특정 시간마다 채용 공고를 가져와서 디스코드 채널에 보내야 한다. 클라우드 서버를 띄워서 하는 것은 배보다 배꼽이 더 큰 상황이다. 이미 Github Actions로 스케줄링 자동화를 구현했던 경험이 있어서 어렵지 않게 구현했다.
Github Actions의 cron을 사용해서, 평일 오후 8시에 src/app.ts가 실행되도록 해두었다. 디스코드 채널에 메시지를 보낸 후에는 process.exit(0)를 호출해서 명시적으로 프로그램 종료를 선언한다. 그렇지 않으면 작업이 끝난 후에도 Github Workflows가 살아있었다.
- .github/workflows/schedule.yml
name: Run schduling jobs for crawling.
on:
schedule:
- cron: "0 11 * * 1-6" # UTC 기준 월~금 11:00(한국 기준 20:00)
push:
branches:
- main
jobs:
run-script:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v1
# Github Reposiroty의 Secret 가져와서 .env 생성하기
- name: Create .env file
run: jq -r 'to_entries|map("\(.key)=\(.value|tostring)")|.[]' <<< "$SECRETS_CONTEXT" > .env
env:
SECRETS_CONTEXT: ${{ toJson(secrets) }}
- name: Install dependencies
run: bun install
- name: Run app.ts
run: bun run scheduling
AI 기반 채용 공고 필터링: Transformers.js
초기 - 키워드 필터링
Transformers.js의 제로샷 파이프라인을 사용해서 채용공고의 카테고리를 분류하고, 필터링하는 기능을 구현했다. 아무래도 AI 동아리이다보니 AI 관련 채용 공고만을 가져오고 싶었다. 기존에는 단순히 공고 제목에 "AI", "ML", "인공지능" 등의 키워드가 포함됐는지를 체크했었는데, 단순 텍스트 비교이다보니 "AI 프로젝트 - Web 개발자", "인공지능 리서치" 등 우리가 원하는 AI 개발 관련 공고와 관련이 없는 공고까지 가져오는 경우가 즐비했다. 어떻게 할까 고민하던 때에, 누군가 인공지능을 적용해보면 어떨까 제안해주었고 자연어 처리 인공지능을 배우기 위해 NLP 스터디에 참여하게 되었다.
자연어 처리 스터디
https://huggingface.co/learn/nlp-course/ko/chapter0/1?fw=pt
Hugging Face라는 인공지능 모델 공유 플랫폼에서 제공하는 NLP 강의를 따르는 스터디에 참여했다. 실습 위주의 NLP 강의와 유튜브의 [CS224n] 라는 LLM 이론 영상들을 보고, 스스로 공부한 것을 인증하는 스터디이다. 끝으로 데이콘의 대회에 참가하는 것을 목표로 한다.
스터디를 통해 인공지능을 사용하는 것이 얼마나 간편해졌는지, 자연어 처리는 어떤 식으로 동작하는지 조금씩이지만 알게 되었다. 그 중에서 채용 공고 필터링에 적합한 Zero-Shot classification 모델을 알게 되었다.
Zero-shot classification 모델 적용
제로샷 모델은 라벨링되지 않은 새로운 라벨에 대한 분류 작업을 수행하는 모델이다. python에서 제공되는 transformers 라이브러리는 모든 NLP 문제를 해결하기 위해 사용된다고 한다. 많은 모델이 있지만, 이번에 나는 제로샷 분류 모델만을 사용해보았다. python 라이브러리라 nodejs에서 사용할 수 있을까 걱정되었지만, 다행히 웹브라우저/Nodejs 로도 제공되어서 간편했다. 확실히 AI가 과거보다 대중적이게 되었다고 느껴진다.
- transformers.ts
import {
pipeline,
PipelineType,
ZeroShotClassificationOutput,
ZeroShotClassificationPipeline,
} from "transformers.js";
export class MyZeroShotClassificationPipeline {
static task: PipelineType = "zero-shot-classification";
static model = "Xenova/nli-deberta-v3-small"; // Hugging Face에서 공개된 여러 모델로 변경할 수 있다.
static instance: ZeroShotClassificationPipeline;
static async getInstance(
progress_callback: Function | undefined = undefined
) {
if (!this.instance) {
console.log(
"학습된 pipeline을 찾을 수 없습니다. 새로운 pipeline을 생성합니다."
);
this.instance = (await pipeline(this.task, this.model, {
progress_callback,
})) as unknown as ZeroShotClassificationPipeline;
}
return this.instance;
}
static async classifyCategory(text: string, categoryList: string[]) {
if (!this.instance) {
console.error(
"pipeline을 찾을 수 없습니다. 새로운 pipeline을 생성합니다."
);
await this.getInstance();
}
console.log(this.instance);
const result = (await this.instance(
text,
categoryList
)) as ZeroShotClassificationOutput;
return result;
}
}
MyZeroShotClassificationPipeline.getInstance();
다만 호환성 문제가 있었는데, Bun에서는 transformers.js 가 정상적으로 동작하지 않는 문제가 있었다. 구글링 결과, bun의 멀티스레드의 병렬 처리 문제로 누군가가 수정해둔 transforemrs.js를 클론해와서 모듈처럼 사용함으로써 해결했다.
"scripts": {
"scheduling": "bun run src/app.ts",
"postinstall": "cd node_modules && git clone https://github.com/sroussey/transformers.js.git && cd transformers.js && git checkout -t origin/fix-node-wasm && npm install && npm run build && cd .."
},
workflows 메모리 덤프
https://docs.github.com/ko/actions/learn-github-actions/usage-limits-billing-and-administration
Github Actions는 깃허브에서 무료로 제공하는 기능인 만큼, 엄연히 사용 제한이 있다. 아마 메모리 사용량에도 제한이 있는 것 같다. 패러미터가 매우 큰 제로샷 모델을 사용하니, Workflows 런타임 중에 메모리 덤프 에러가 발생했다.
성능을 좀 감안하고 Hugging Face에서 패러미터가 작은 모델을 찾아 적용하니 해결됐다.
결론
구현이 끝났다. 초기에는 키워드 필터링으로 냅뒀지만, 기왕 인공지능 동아리에 들어온 거, 인공지능이라도 배워서 프로젝트에 써먹어봐야겠단 심산에 자연어 처리 스터디도 참여하고 강의도 들으며 열심히 인공지능을 공부해봤다. 확실히 2년 전과는 달리 배우기 쉬운 자료가 많아져서 공부하기 수월해졌단 생각을 한다.
한국어로 파인튜닝된 모델이 아니라서 한국어 Classification 성능은 아직 좀 부족한 것 같아서, 다음에 기회가 된다면 한국어 파인튜닝까지 시도해보고 싶다.
'프로젝트 > 인공지능 동아리, AID' 카테고리의 다른 글
AID 모각코 다녀옴 (0) | 2024.01.23 |
---|---|
[팀프로젝트] AID 홈페이지 리뉴얼 - (3) API 모킹 (3) | 2024.01.22 |
[팀프로젝트] AID 홈페이지 리뉴얼 - (2) 코드 가독성을 높이기 위한 전략 (1) | 2023.12.28 |
[팀프로젝트] AID 홈페이지 리뉴얼 - (1) 기획 및 큰 틀 구현 (0) | 2023.12.16 |