diff --git a/Backend/Sentimental/__main__.py b/Backend/Sentimental/__main__.py index 8b63f2b..6dac815 100644 --- a/Backend/Sentimental/__main__.py +++ b/Backend/Sentimental/__main__.py @@ -1,4 +1,4 @@ if __name__ == "__main__": import uvicorn - uvicorn.run("app.main:app", host="0.0.0.0", port=30007, reload=True) + uvicorn.run("Sentimental.main:app", host="0.0.0.0", port=30007, reload=True) diff --git a/Backend/Sentimental/main.py b/Backend/Sentimental/main.py index f85368b..96d6e15 100644 --- a/Backend/Sentimental/main.py +++ b/Backend/Sentimental/main.py @@ -11,66 +11,94 @@ app = FastAPI() -MODEL_PATH = "/opt/ml/input/model-roberta_large-sota_trainer" -tokenizer = AutoTokenizer.from_pretrained("klue/roberta-large") +MODEL_PATH = "/opt/ml/outputs/klue/roberta-large_merged-4_100-26_100" + model = AutoModelForSequenceClassification.from_pretrained(MODEL_PATH) +tokenizer = AutoTokenizer.from_pretrained("klue/roberta-large") +special_tokens_dict = {'additional_special_tokens': ['[COMPANY]','[/COMPANY]']} +num_added_toks = tokenizer.add_special_tokens(special_tokens_dict) -@app.get("/") -def hello_world(): - return {"hello": "world"} +model.resize_token_embeddings(len(tokenizer)) +device = 'cuda' if torch.cuda.is_available() else 'cpu' +model = model.to(device) -def predict_sentiment(text): - model.eval() - with torch.no_grad() : - temp = tokenizer( - text, - return_tensors='pt', - padding=True, - truncation=True, - ## - max_length=100, - # stride=stride, - # return_overflowing_tokens=True, - return_offsets_mapping=False - ) - - - predicted_label = model(input_ids=temp['input_ids'], - token_type_ids=temp['token_type_ids']) - - print(predicted_label) - - results = [] - results = torch.nn.Softmax(dim=-1)(predicted_label.logits) +def extract_sentences_token(input_dict, pad_token_id): + ''' + 512 토큰 초과라면 input_ids, token_type_ids, attention_maks를 + 앞 128, 뒤 384으로 분리합니다. + 이하라면 뒤는 pad 토큰으로 채웁니다. + 사용 방법: + train_encoding = tokenizer(text) + train_encoding = extract_sentences_token(train_encoding, tokenizer.pad_token_id) + ''' + new = {} + batch_size = len(input_dict['input_ids']) + new['input_ids'] = torch.ones(batch_size, 512, dtype=int) + new['token_type_ids'] = torch.ones(batch_size, 512, dtype=int) + new['attention_mask'] = torch.ones(batch_size, 512, dtype=int) + # batch_size, 512 + for i in range(batch_size): + a = input_dict['input_ids'][i] + a = a[a != pad_token_id] + length = len(a) + if length > 512: + left, right = 1, 3 + a = torch.cat((a[:128*left], a[-128*right:]), dim=0) + new['input_ids'][i] = a + new['token_type_ids'][i] = input_dict['token_type_ids'][i][:512] + new['attention_mask'][i] = input_dict['attention_mask'][i][:512] + else: + new['input_ids'][i] = input_dict['input_ids'][i][:512] + new['token_type_ids'][i] = input_dict['token_type_ids'][i][:512] + new['attention_mask'][i] = input_dict['attention_mask'][i][:512] + return new +def predict_sentiment(text: List[str]) -> List[str]: answer = [] - print(results) - for result in results : - if result[0]>=result[1] : - answer.append("부정") - - else : - answer.append("긍정") - + model.eval() + + loader = torch.utils.data.DataLoader(dataset=text, batch_size=32, shuffle=False) + with torch.no_grad() : + for batch_text in loader: + temp = tokenizer( + batch_text, + return_tensors='pt', + padding="max_length", + truncation=True, + max_length=3000, # 충분히 커서 모두 토큰화할 길이 + ) + temp = extract_sentences_token(temp, tokenizer.pad_token_id) + if torch.cuda.is_available(): + temp = {key: value.to(device) for key, value in temp.items()} + predicted_label = model(**temp) + + results = torch.nn.Softmax(dim=-1)(predicted_label.logits) + for result in results : + if result[0]>=result[1] : + answer.append("부정") + else : + answer.append("긍정") + return answer class FinanaceSentiment(BaseModel): corpus_list: list = [] - title: str = "title" - company: str = "삼성전자" - result: Optional[List] + company_list: list = [] + @app.post("/classify_sentiment", description="문장의 감정을 분류합니다.") async def classify_sentiment(finance: FinanaceSentiment): # 입력으로 받은 텍스트를 모델로 예측합니다. - predictions = predict_sentiment(finance.corpus_list) + input = [] + for corpus, company in zip(finance.corpus_list, finance.company_list) : + input.append(f"이 기사는[COMPANY]{company}[/COMPANY]에 대한 기사야. {corpus}") + + predictions = predict_sentiment(input) # 결과를 반환합니다. result = { - "title": finance.title, - # "input_text": finance.corpus, "sentiment": predictions } diff --git a/Frontend/deguel/index.html b/Frontend/deguel/index.html index 79c4701..8e45866 100644 --- a/Frontend/deguel/index.html +++ b/Frontend/deguel/index.html @@ -4,7 +4,7 @@ - Vite + React + 뉴키주 - 뉴스 키워드 주식
diff --git a/Frontend/deguel/src/App.jsx b/Frontend/deguel/src/App.jsx index f4df269..2b2011f 100644 --- a/Frontend/deguel/src/App.jsx +++ b/Frontend/deguel/src/App.jsx @@ -2,25 +2,22 @@ import { Title, - Text, Tab, TabList, TabGroup, TabPanel, TabPanels, - Grid, - Col, - Card, + Metric, } from "@tremor/react"; + import TreemMap from "./elements/TreeMap"; -import SummaryCard from "./elements/Summary"; export default function App() { return (
- Deguel - 현재, 언론사에서 가장 많이 다루고 있는 키워드 입니다. + 뉴키주 + 현재, 언론사에서 가장 많이 다루고 있는 키워드 입니다. @@ -42,27 +39,6 @@ export default function App() { - - 사용자 추천 기사 - 많은 사용자가 본 뉴스에요. - - - - - -
- - - - - - - -
- - - -
); } diff --git a/Frontend/deguel/src/elements/Chart.jsx b/Frontend/deguel/src/elements/Chart.jsx index d6ed1e4..358a52a 100644 --- a/Frontend/deguel/src/elements/Chart.jsx +++ b/Frontend/deguel/src/elements/Chart.jsx @@ -14,17 +14,21 @@ import { import { PropTypes } from 'prop-types'; import { useEffect, useState } from "react"; -import { startOfYear, subDays } from "date-fns"; + import { supabase } from "../supabaseClient"; +import { startOfYear, subDays } from "date-fns"; + + // 원화 표기를 위한 함수. const dataFormatter = (number) => `${Intl.NumberFormat("ko-KR", { style: 'currency', currency: 'KRW', }).format(number).toString()}`; + export default function LineChartTab(props) { - const [selectedIndex, setSelectedIndex] = useState("Max"); + const [selectedIndex, setSelectedIndex] = useState("4"); const [stockPrice, setStockPrice] = useState([{"id": 0, "ticker": "005930", "price": 0, "date": "2021-04-05"}, {"id": 0, "ticker": "005930", "price": 0, "date": "2021-04-05"}]); const [ticker_name, setTickerName] = useState("a"); @@ -33,6 +37,7 @@ export default function LineChartTab(props) { getInformations(); }, []); + // LineChartTab의 prop 값 설정. LineChartTab.propTypes = { ticker: PropTypes.string.isRequired, } @@ -41,6 +46,11 @@ export default function LineChartTab(props) { async function getInformations() { // DB에서 ticker에 맞는 가격 데이터 가져오기. var { data } = await supabase.from("price").select("*").eq("ticker", props.ticker).order('date', { ascending: true }); + + for(let i = 0;i < data.length;i++) { + data[i].date = data[i].date.split("T")[0].slice(); + } + setStockPrice(data); // ticker에 맞는 주가 명 가져오기. @@ -101,7 +111,7 @@ export default function LineChartTab(props) { {dataFormatter(stockPrice[stockPrice.length - 1].price)} - 최근 1주일간의 주식 차트에요! + 선택하신 기간 동안의 주식 차트에요! 1M @@ -121,6 +131,7 @@ export default function LineChartTab(props) { valueFormatter={dataFormatter} showLegend={false} yAxisWidth={48} + autoMinValue={true} /> @@ -133,6 +144,7 @@ export default function LineChartTab(props) { valueFormatter={dataFormatter} showLegend={false} yAxisWidth={48} + autoMinValue={true} /> @@ -145,6 +157,7 @@ export default function LineChartTab(props) { valueFormatter={dataFormatter} showLegend={false} yAxisWidth={48} + autoMinValue={true} /> @@ -157,6 +170,7 @@ export default function LineChartTab(props) { valueFormatter={dataFormatter} showLegend={false} yAxisWidth={48} + autoMinValue={true} /> @@ -169,6 +183,7 @@ export default function LineChartTab(props) { valueFormatter={dataFormatter} showLegend={false} yAxisWidth={48} + autoMinValue={true} /> diff --git a/Frontend/deguel/src/elements/Summary.jsx b/Frontend/deguel/src/elements/Summary.jsx index 015ce54..f20768e 100644 --- a/Frontend/deguel/src/elements/Summary.jsx +++ b/Frontend/deguel/src/elements/Summary.jsx @@ -1,4 +1,3 @@ -import { useEffect, useState } from "react"; import { Title, Subtitle, @@ -7,67 +6,115 @@ import { Divider, Flex, BadgeDelta, - Button, } from "@tremor/react" -import { supabase } from "../supabaseClient"; import { PropTypes } from 'prop-types'; +import { useEffect, useState } from "react"; -const data = [ - { color: "gray", tooltip: "Operational" }, - { color: "gray", tooltip: "Operational" }, - { color: "gray", tooltip: "Operational" }, - { color: "blue", tooltip: "Operational" }, - { color: "gray", tooltip: "Operational" }, - { color: "gray", tooltip: "Operational" }, - { color: "gray", tooltip: "Operational" }, - { color: "gray", tooltip: "Operational" }, - { color: "gray", tooltip: "Operational" }, - { color: "gray", tooltip: "Operational" }, - { color: "gray", tooltip: "Operational" }, - { color: "gray", tooltip: "Operational" }, - { color: "gray", tooltip: "Operational" }, - { color: "gray", tooltip: "Operational" }, - { color: "gray", tooltip: "Operational" }, - { color: "gray", tooltip: "Operational" }, - { color: "blue", tooltip: "Operational" }, - { color: "green", tooltip: "Operational" }, - { color: "green", tooltip: "Operational" }, - { color: "green", tooltip: "Downtime" }, - { color: "blue", tooltip: "Operational" }, - { color: "gray", tooltip: "Operational" }, - { color: "gray", tooltip: "Operational" }, - { color: "gray", tooltip: "Operational" }, - { color: "gray", tooltip: "Maintenance" }, - { color: "gray", tooltip: "Operational" }, - { color: "gray", tooltip: "Operational" }, - { color: "blue", tooltip: "Operational" }, - { color: "green", tooltip: "Degraded" }, - { color: "green", tooltip: "Operational" }, +import { supabase } from "../supabaseClient"; + +const data_tracker = [ + { color: "gray", tooltip: "언급 없음" }, + { color: "gray", tooltip: "언급 없음" }, + { color: "gray", tooltip: "언급 없음" }, + { color: "gray", tooltip: "언급 없음" }, + { color: "gray", tooltip: "언급 없음" }, + { color: "gray", tooltip: "언급 없음" }, + { color: "gray", tooltip: "언급 없음" }, + { color: "gray", tooltip: "언급 없음" }, + { color: "gray", tooltip: "언급 없음" }, + { color: "gray", tooltip: "언급 없음" }, + { color: "gray", tooltip: "언급 없음" }, + { color: "gray", tooltip: "언급 없음" }, + { color: "gray", tooltip: "언급 없음" }, + { color: "gray", tooltip: "언급 없음" }, + { color: "gray", tooltip: "언급 없음" }, + { color: "gray", tooltip: "언급 없음" }, + { color: "gray", tooltip: "언급 없음" }, + { color: "gray", tooltip: "언급 없음" }, + { color: "gray", tooltip: "언급 없음" }, + { color: "gray", tooltip: "언급 없음" }, + { color: "gray", tooltip: "언급 없음" }, + { color: "gray", tooltip: "언급 없음" }, + { color: "gray", tooltip: "언급 없음" }, + { color: "gray", tooltip: "언급 없음" }, + { color: "gray", tooltip: "언급 없음" }, + { color: "gray", tooltip: "언급 없음" }, + { color: "gray", tooltip: "언급 없음" }, + { color: "gray", tooltip: "언급 없음" }, + { color: "gray", tooltip: "언급 없음" }, + { color: "gray", tooltip: "언급 없음" }, ]; export default function SummaryCard(props) { const [newsSummarized, setNewsSummarized] = useState("") const [keyword, setKeyword] = useState(props.keyword) - - // useEffect로 - // const [newsKeywordList, setnewsKeywordList] = useState([]) + const [dataTracker, setDataTracker] = useState(data_tracker) + const [href, setHref] = useState("") // useEffect로 함수 call. useEffect(() => { getInformations(); }, []); + // SummaryCard의 props 설정. SummaryCard.propTypes = { keyword: PropTypes.string.isRequired, + color: PropTypes.string.isRequired, isMain: PropTypes.bool.isRequired, } + // start와 end 사이의 날짜 list 반환 함수. + const getDateList = (start, end) => { + for(var arr=[],dt=new Date(start); dt<=new Date(end); dt.setDate(dt.getDate()+1)){ + arr.push(new Date(dt)); + } + return arr; + } // Supabase에서 데이터 가져오기. async function getInformations() { - const { data } = await supabase.from("news").select("content").order('date', { ascending: false }).limit(1); - setNewsSummarized(data[0].content); + console.log("keyword :" + props.keyword); + let { data } = await supabase.from("keywords").select("summary_id").eq("keyword", props.keyword); + let id = ""; + + for(let d of data) { + id = props.color === "green" ? d.summary_id.pos_news[0] : d.summary_id.neg_news[0]; + + data = await supabase.from("news_summary").select("summarization").eq("origin_id", id); + if(data.data[0].summarization !== ""){ + setNewsSummarized(data.data[0].summarization); + break; + } + } + + data = await supabase.from("keywords").select("create_time").eq("keyword", props.keyword); + + const list_date = getDateList(new Date() - 30 * 24 * 60 * 60 * 1000, new Date()); + data = data.data.map(item => item.create_time.split("T")[0]); + + const newDataTracker = JSON.parse(JSON.stringify(data_tracker)); + + // activation graph 적용. + for(const [index, item] of newDataTracker.entries()){ + const year = list_date[index].getFullYear(); + const month = String(list_date[index].getMonth() + 1).padStart(2, '0'); // 월은 0부터 시작하므로 +1을 해주고, 두 자리 숫자로 포맷팅 + const day = String(list_date[index].getDate()).padStart(2, '0'); // 두 자리 숫자로 포맷팅 + + // `2023-07-25` 형식으로 변환 + const formattedDate = `${year}-${month}-${day}` + ""; + newDataTracker[index].tooltip = formattedDate; + + if(data.includes(formattedDate)) { + newDataTracker[index].color = "green"; + } else { + newDataTracker[index].color = "gray"; + } + } + data = await supabase.from("news").select("link").eq("id", id); + setHref(data.data[0].link); + + setDataTracker(newDataTracker); } // Click event for Close button. @@ -78,17 +125,16 @@ export default function SummaryCard(props) { return (
- 선택하신 키워드에 대한 주요 뉴스 요약본이에요! + {!props.isMain && 선택하신 키워드에 대한 주요 뉴스 요약본이에요!} # {keyword} - {newsSummarized} - - + {newsSummarized} {!props.isMain &&
- 해당 키워드의 지난 30일간 언급 유무예요. - 언급 빈도 - - + + 해당 키워드의 지난 30일간 언급 유무예요. + 언급 빈도 + + 같이 언급된 키워드예요. 연관 키워드 @@ -99,20 +145,8 @@ export default function SummaryCard(props) { onCloseClick("조사")}>조사 onCloseClick("TSMC")}>TSMC -
} - - 이 기사를 추천하시나요? - 추천하기 -
-
- -
-
- -
-
); } diff --git a/Frontend/deguel/src/elements/TreeMap.jsx b/Frontend/deguel/src/elements/TreeMap.jsx index ec74640..0b95223 100644 --- a/Frontend/deguel/src/elements/TreeMap.jsx +++ b/Frontend/deguel/src/elements/TreeMap.jsx @@ -1,34 +1,34 @@ import { - Chart as ChartJS, - CategoryScale, - LinearScale, - BarElement, - Title, - Tooltip, - Legend, -} from 'chart.js'; -import { TreemapController, TreemapElement } from 'chartjs-chart-treemap'; -import { Chart, getElementAtEvent } from 'react-chartjs-2'; - -import { - Title as TremorTitle, Subtitle, Button, Divider, Grid, Col, + DateRangePicker, } from "@tremor/react"; +import { ArrowLeftIcon } from "@heroicons/react/outline"; +import { ko } from "date-fns/locale"; -import { color } from 'chart.js/helpers'; import { PropTypes } from 'prop-types'; import { useEffect, useRef, useState } from 'react'; -import LineChartTab from './Chart'; +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + BarElement, + Title, + Tooltip, + Legend, +} from 'chart.js'; +import { TreemapController, TreemapElement } from 'chartjs-chart-treemap'; +import { Chart, getElementAtEvent } from 'react-chartjs-2'; +import { color } from 'chart.js/helpers'; import { supabase } from '../supabaseClient'; -import { ArrowLeftIcon } from "@heroicons/react/outline"; +import LineChartTab from './Chart'; import SummaryCard from './Summary'; ChartJS.register( @@ -42,66 +42,183 @@ ChartJS.register( TreemapElement ); +function shuffle(array) { + array.sort(() => Math.random() - 0.5); +} + export default function TreeMap(props) { + const midnight = new Date(); + midnight.setDate(midnight.getDate() - 1); const chartRef = useRef(); const [kewords, setKeywords] = useState([]); const [isClicked, setIsClicked] = useState(false); - const [clickedKeyword, setClickedKeyword] = useState("") + const [clickedKeyword, setClickedKeyword] = useState(""); + const [clickedTicker, setClickedTicker] = useState(""); + const [updatedTime, setUpdatedTime] = useState(""); + const [windowSize, setWindowSize] = useState([ + window.innerWidth, + window.innerHeight, + ]); + const [neg_cntsum, setNeg_cntsum] = useState(0); + const [pos_cntsum, setPos_cntsum] = useState(0); + const [dateRange, setDateRange] = useState({ + from: midnight, + to: new Date(), + }); // useEffect로 함수 call. useEffect(() => { getInformations(); - }, []); + + const handleWindowResize = () => { + setWindowSize([window.innerWidth, window.innerHeight]); + }; + + window.addEventListener('resize', handleWindowResize); + return () => { + window.removeEventListener('resize', handleWindowResize); + }; + }, [neg_cntsum, pos_cntsum, dateRange]); // Supabase에서 데이터 가져오기. async function getInformations() { - const { data } = await supabase.from("keywords").select("*").order('create_time', { ascending: false }); + let { data } = await supabase.from("keywords").select("*").order('create_time', { ascending: false }).limit(1); + // data = await supabase.from("keywords").select("*").eq("create_time", data[0].create_time).order('create_time', { ascending: false }); + // data = await supabase.from("keywords").select("*") + // .gte("create_time", midnight.toISOString()) + // .lte("create_time", new Date().toISOString()) + // .order('create_time', { ascending: false }); + data = await supabase.from("keywords").select("*") + .gte("create_time", dateRange.from.toISOString()) + .lte("create_time", dateRange.to.toISOString()) + .order('create_time', { ascending: false }); + data = data.data + + // Create an object to store the combined data + const combinedData = data.reduce((result, current) => { + // Check if the keyword already exists in the result object + if (result[current.keyword]) { + // If yes, add the count to the existing keyword + result[current.keyword].count += current.count; + result[current.keyword].pos_cnt += current.pos_cnt; + result[current.keyword].neg_cnt += current.neg_cnt; + result[current.keyword].ratio_pos += current.ratio_pos; + result[current.keyword].ratio_neg += current.ratio_neg; + + result[current.keyword].ratio_neg += current.ratio_neg; + } else { + // If not, create a new entry for the keyword + result[current.keyword] = { ...current }; + } + return result; + }, {}); + + // Convert the combinedData object back to an array of values + data = Object.values(combinedData); + + console.log(data); + + // const { data } = await supabase.from("keywords").select("*").order('create_time', { ascending: false }); + let neg = 0, pos = 0 + + for(let i = 0;i < data.length;i++) { + if(data[i].pos_cnt >= data[i].neg_cnt) { + data[i].ratio_pos = (data[i].pos_cnt - data[i].neg_cnt) * data[i].pos_cnt; + data[i].ratio_neg = 0; + } + if(data[i].pos_cnt <= data[i].neg_cnt) { + data[i].ratio_neg = (data[i].neg_cnt - data[i].pos_cnt) * data[i].neg_cnt; + data[i].ratio_pos = 0; + } + // data[i].ratio_pos = data[i].pos_cnt; + // data[i].ratio_neg = data[i].neg_cnt; + neg += data[i].neg_cnt; + pos += data[i].pos_cnt; + } + setNeg_cntsum(neg); + setPos_cntsum(pos); console.log(data); + console.log("neg_cntsum: " + neg_cntsum); + console.log("pos_cntsum: " + pos_cntsum); + + shuffle(data); setKeywords(data); + + var time_formated = data[0].create_time.replace("T", " ").split("+")[0].split(":")[0] + time_formated = time_formated.split("-")[0] + "년 " + + time_formated.split("-")[1] + "월 " + + time_formated.split("-")[2].split(" ")[0] + "일 " + + time_formated.split("-")[2].split(" ")[1] + "시" + setUpdatedTime(time_formated); + + // dateRange.to.setDate(dateRange.to.getDate() + 1); + // const range = { + // date_start: dateRange.from.toISOString().slice(0, 19).replace('T', ' '), + // date_end: dateRange.to.toISOString().slice(0, 19).replace('T', ' '), + // }; + // const url = "http://118.67.133.198:30008/"; + // console.log(url); + + // // fetch를 사용하여 POST 요청 보내기 + // await fetch(url, { + // method: "get", + // // mode: "no-cors", + // }) + // // .then(response => response.json()) // JSON 형태의 응답을 파싱 + // .then(result => { + // // 요청에 대한 처리 + // console.log("결과:", result); + // }) + // .catch(error => { + // console.error("오류 발생:", error); + // }); } + // TreeMap의 prop 설정. TreeMap.propTypes = { color: PropTypes.string.isRequired, title: PropTypes.string.isRequired, } + // TreeMap의 디자인 설정. const config = { type: 'treemap', data: { datasets: [ { tree: kewords, - key: props.color === "tomato" ? 'neg_cnt' : 'pos_cnt', + key: props.color === "tomato" ? 'ratio_neg' : 'ratio_pos', labels: { display: true, formatter: (context) => context.raw._data.keyword, color: ['white', 'whiteSmoke'], - font: [{size: 36, weight: 'bold'}, {size: 12}], + font: [{size: 20, weight: 'bold'}, {size: 12}], }, backgroundColor(context) { if (context.type !== 'data') return 'transparent'; - const { count, pos_cnt, neg_cnt } = context.raw._data; + const { count, pos_cnt, neg_cnt, ratio_neg, ratio_pos } = context.raw._data; if(props.color === "green") return pos_cnt / count === 0 ? color('grey').rgbString() - : color(props.color).alpha(pos_cnt / count).rgbString(); + : color(props.color).alpha((ratio_pos) / pos_cntsum * 30).rgbString(); if(props.color === "tomato") return neg_cnt / count === 0 ? color('grey').rgbString() - : color(props.color).alpha(neg_cnt / count).rgbString(); + : color(props.color).alpha((ratio_neg) / neg_cntsum * 30).rgbString(); }, }, ], }, }; + // TreeMap의 Tooltip 세부 설정. const options = { plugins: { title: { display: true, - text: '분석된 키워드', + text: '키워드를 클릭 해보세요!', }, legend: { display: false, @@ -128,12 +245,23 @@ export default function TreeMap(props) { }; // Click event for TreeMap. - const onDataClick = (event) => { + const onDataClick = async (event) => { if(chartRef.current) { - console.log(event); - console.log(chartRef); const clicked_text = getElementAtEvent(chartRef.current, event)[0].element.options.labels.formatter; + for(let keyword of kewords) { + if(keyword.keyword === clicked_text) { + const idx = props.color === "green" ? keyword.summary_id.pos_news[0] : keyword.summary_id.neg_news[0]; + + let { data } = await supabase.from("news_summary").select("origin_id").eq("origin_id", idx); + data = await supabase.from("news").select("company").eq("id", data[0].origin_id); + data = await supabase.from("ticker").select("ticker").eq("name", data.data[0].company); + + setClickedTicker(data.data[0].ticker); + break; + } + } + setClickedKeyword(clicked_text); setIsClicked(true); } @@ -144,24 +272,45 @@ export default function TreeMap(props) { setIsClicked(false); } + // Get the width of TreeMap. + const getTreeMapWidth = (width) => { + if(width > 1024) { + return 200; + } else if (width > 720) { + return 800; + } else { + return 1000; + } + } + return (
{!isClicked &&
- 분석된 {props.title} 예요. - {props.title} - + + + {updatedTime}에 분석된 {props.title} 예요. +
} {isClicked &&
- - - + + +
- +
diff --git a/Frontend/deguel/src/elements/ValueCard.jsx b/Frontend/deguel/src/elements/ValueCard.jsx deleted file mode 100644 index 04e3eb3..0000000 --- a/Frontend/deguel/src/elements/ValueCard.jsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Card, Text, Metric, Flex, ProgressBar } from "@tremor/react"; - -export default function ValueCard(){ - return - Sales - $ 71,465 - - 32% of annual target - $ 225,000 - - - -} \ No newline at end of file diff --git a/Notebooks/Sentimental/glob.ipynb b/Notebooks/Sentimental/glob.ipynb new file mode 100644 index 0000000..485d96b --- /dev/null +++ b/Notebooks/Sentimental/glob.ipynb @@ -0,0 +1,173 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "import os" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 회사명 기준으로 파일 나누기\n", + "preprocessed_data_로 시작하는 chatgpt 라벨링 데이터를 회사명으로 분류해서 merged_(회사명) 으로 저장" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "def merge_and_move_files(directory_path):\n", + " file_list = os.listdir(directory_path)\n", + "\n", + " files_by_company = {}\n", + "\n", + " # 파일들을 기업 이름에 따라 그룹화\n", + " for file_name in file_list:\n", + " # if file_name[0].isdigit():\n", + " if file_name.startswith(\"preprocessed_data_\"):\n", + " file_path = os.path.join(directory_path, file_name)\n", + " data = pd.read_csv(file_path)\n", + " \n", + " company_name = data['company'].iloc[0]\n", + " \n", + " if company_name not in files_by_company:\n", + " files_by_company[company_name] = []\n", + " \n", + " files_by_company[company_name].append(file_name)\n", + "\n", + " # 각 기업에 대해 폴더를 만들고 해당 폴더로 각각 파일들을 저장\n", + " for company_name, files in files_by_company.items():\n", + " folder_path = os.path.join(directory_path, \"merged\")\n", + " os.makedirs(folder_path, exist_ok=True)\n", + "\n", + " combined_data = pd.concat([pd.read_csv(os.path.join(directory_path, file)) for file in files], axis=0)\n", + "\n", + " merged_file_path = os.path.join(folder_path, f\"merged_{company_name}.csv\")\n", + " combined_data.to_csv(merged_file_path, index=False)\n", + " \n", + "\n", + "if __name__ == \"__main__\":\n", + " target_directory = \"/opt/ml/finance_sentiment_corpus\"\n", + " merge_and_move_files(target_directory)\n" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 파일 합치기\n", + "merged_(회사명)을 학습 시키기위한 merged_all.csv 만들기" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def merge_csv_files(directory_path, output_file_name):\n", + " file_list = os.listdir(directory_path)\n", + " combined_data = pd.DataFrame()\n", + "\n", + " for file_name in file_list:\n", + " if file_name.endswith(\".csv\") and file_name != \"merged_all.csv\" : # merged_all.csv 제외하고 합치기\n", + " file_path = os.path.join(directory_path, file_name)\n", + "\n", + " data = pd.read_csv(file_path)\n", + " \n", + " # 혹여나 \"labels\"로 지정해둔 column 이름 변경\n", + " if \"labels\" in data.columns and \"label\" not in data.columns :\n", + " data[\"label\"] = data[\"labels\"]\n", + " \n", + " data = data[[\"company\", \"title\", \"date\", \"content_corpus\", \"label\"]]\n", + " combined_data = pd.concat([combined_data, data], axis=0, ignore_index=True)\n", + " \n", + " output_file_path = os.path.join(directory_path, output_file_name)\n", + "\n", + " # 지우고 다시 만들기\n", + " if os.path.exists(output_file_path):\n", + " os.remove(output_file_path)\n", + " \n", + " combined_data.to_csv(output_file_path, index=False)\n", + " return combined_data\n", + "\n", + "directory_path = \"/opt/ml/finance_sentiment_corpus/merged\"\n", + "output_file_name = \"merged_all.csv\"\n", + "output_file_path = directory_path + output_file_name\n", + " \n", + "data = merge_csv_files(directory_path , output_file_name)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## TEST 데이터셋 만들기\n", + "26개의 회사에서 20개씩 뽑아 test_dataset 만들기" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "csv_file_path = \"/opt/ml/finance_sentiment_corpus/label_0_to_521.csv\"\n", + "df = pd.read_csv(csv_file_path)\n", + "\n", + "# 새로운 DataFrame을 저장할 리스트 생성\n", + "new_dfs = []\n", + "\n", + "# company 컬럼의 고유한 값들을 추출하여 각 회사별로 20개씩 행을 샘플링하여 새로운 DataFrame으로 생성\n", + "for company_name in df['company'].unique():\n", + " company_subset = df[df['company'] == company_name].sample(n=10, random_state=42) # 20개씩 랜덤 샘플링 (여기서는 random_state를 고정하여 재현성을 위해 사용)\n", + " new_dfs.append(company_subset)\n", + "\n", + "# 새로운 DataFrame을 병합하여 하나의 DataFrame으로 합치기\n", + "result_df = pd.concat(new_dfs)\n", + "\n", + "# 새로운 DataFrame을 CSV 파일로 저장\n", + "result_csv_file_path = \"/opt/ml/finance_sentiment_corpus/26_company_half_labeled.csv\" # 저장할 파일 경로 설정 (적절하게 변경해주세요)\n", + "result_df.to_csv(result_csv_file_path, index=False) # index=False를 지정하여 인덱스를 저장하지 않도록 설정합니다.\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "final", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.5" + }, + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "981f108a204f421f158e0977940335d851edffa6dd3586828a3e1aec045160e4" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/Notebooks/datasets.ipynb b/Notebooks/datasets.ipynb deleted file mode 100644 index 610a130..0000000 --- a/Notebooks/datasets.ipynb +++ /dev/null @@ -1,561 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/opt/conda/envs/mrc/lib/python3.10/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", - " from .autonotebook import tqdm as notebook_tqdm\n" - ] - } - ], - "source": [ - "import torch\n", - "import pandas as pd\n", - "import sklearn\n", - "\n", - "from transformers import AutoModelForSequenceClassification, AutoTokenizer\n", - "from transformers import TrainingArguments, Trainer\n", - "from transformers import pipeline\n", - "\n", - "import sys\n", - "sys.path.append('../sentence-sentimental')\n", - "\n", - "from dataset.datasets import SentimentalDataset\n", - "from metrics.metrics import compute_metrics\n", - "\n", - "from sklearn.datasets import load_iris # 샘플 데이터 로딩\n", - "from sklearn.model_selection import train_test_split" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Some weights of the model checkpoint at klue/roberta-base were not used when initializing RobertaForSequenceClassification: ['lm_head.layer_norm.bias', 'lm_head.layer_norm.weight', 'lm_head.dense.bias', 'lm_head.decoder.bias', 'lm_head.decoder.weight', 'lm_head.dense.weight', 'lm_head.bias']\n", - "- This IS expected if you are initializing RobertaForSequenceClassification from the checkpoint of a model trained on another task or with another architecture (e.g. initializing a BertForSequenceClassification model from a BertForPreTraining model).\n", - "- This IS NOT expected if you are initializing RobertaForSequenceClassification from the checkpoint of a model that you expect to be exactly identical (initializing a BertForSequenceClassification model from a BertForSequenceClassification model).\n", - "Some weights of RobertaForSequenceClassification were not initialized from the model checkpoint at klue/roberta-base and are newly initialized: ['classifier.out_proj.bias', 'classifier.out_proj.weight', 'classifier.dense.weight', 'classifier.dense.bias']\n", - "You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference.\n" - ] - } - ], - "source": [ - "device = torch.device(\"cuda\" if torch.cuda.is_available() else 'cpu')\n", - "MODEL_NAME = 'klue/roberta-base'\n", - "model = AutoModelForSequenceClassification.from_pretrained(MODEL_NAME, num_labels=3).to(device)\n", - "tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "data = pd.read_csv('/opt/ml/finance_sentiment_corpus/finance_data.csv')\n", - "data['labels'] = data['labels'].map({'negative':0, 'neutral':1, 'positive':2})\n", - "\n", - "train_encoding = tokenizer(\n", - " data['kor_sentence'].tolist(),\n", - " return_tensors='pt',\n", - " padding=True,\n", - " truncation=True\n", - " )\n", - "\n", - "train_set = SentimentalDataset(train_encoding, data['labels'])" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "pandas.core.frame.DataFrame" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "type(data)" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [], - "source": [ - "import pandas as pd\n", - "from sklearn.datasets import load_wine\n", - "\n", - "wine = load_wine()" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [], - "source": [ - "X = wine['data']\n", - "Y = wine['target']" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [], - "source": [ - "X_train, X_test, y_train, y_test = train_test_split(X, Y, stratify=Y, random_state=777)" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "numpy.ndarray" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "type(X_train)" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "numpy.ndarray" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "type(X)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "4846 4846\n" - ] - } - ], - "source": [ - "print(len(data['kor_sentence']), len(data['labels']))" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "pandas.core.series.Series" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "type(data['kor_sentence'])" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "0 Gran에 따르면, 그 회사는 회사가 성장하고 있는 곳이지만, 모든 생산을 러시아로...\n", - "1 테크노폴리스는 컴퓨터 기술과 통신 분야에서 일하는 회사들을 유치하기 위해 10만 평...\n", - "2 국제 전자산업 회사인 엘코텍은 탈린 공장에서 수십 명의 직원을 해고했으며, 이전의 ...\n", - "3 새로운 생산공장으로 인해 회사는 예상되는 수요 증가를 충족시킬 수 있는 능력을 증가...\n", - "4 2009-2012년 회사의 업데이트된 전략에 따르면, Basware는 20% - 4...\n", - " ... \n", - "4841 런던 마켓워치 -- 은행주의 반등이 FTSE 100지수의 약세를 상쇄하지 못하면서 ...\n", - "4842 린쿠스키아의 맥주 판매량은 416만 리터로 6.5% 감소했으며 카우노 알루스의 맥주...\n", - "4843 영업이익은 2007년 68.8 mn에서 35.4 mn으로 떨어졌으며, 선박 판매 이...\n", - "4844 페이퍼 부문 순매출은 2008년 2분기 241.1 mn에서 2009년 2분기 221...\n", - "4845 핀란드에서의 판매는 1월에 10.5% 감소한 반면, 국외에서의 판매는 17% 감소했다.\n", - "Name: kor_sentence, Length: 4846, dtype: object" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "data['kor_sentence']" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [], - "source": [ - "sentence_train, sentence_test, label_train, label_test = train_test_split(data['kor_sentence'], data['labels'],\n", - " test_size=0.2, \n", - " # shuffle=True, \n", - " stratify=data['labels'], # label에 비율을 맞춰서 분리\n", - " random_state=34)" - ] - }, - { - "cell_type": "code", - "execution_count": 41, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "pandas.core.series.Series" - ] - }, - "execution_count": 41, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "type(data['kor_sentence'])" - ] - }, - { - "cell_type": "code", - "execution_count": 39, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "pandas.core.series.Series" - ] - }, - "execution_count": 39, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "type(label_train)" - ] - }, - { - "cell_type": "code", - "execution_count": 26, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "1828 핀란드 식품업계 기업들은 우크라이나가 제공하는 기회에 관심이 많다.\n", - "4809 펜틱+아이넨은 미디어하우스가 제공하는 인터넷 콘텐츠 대부분이 영원히 무료일 수는 없...\n", - "1166 또한 이러한 시장에 대한 6년간의 역사적 분석이 제공됩니다.\n", - "277 루키 총리는 핀란드 사본리나에 위치한 키론살미 해협을 가로지르는 철제 구조물의 인도...\n", - "4670 2009년 8월 3일 핀란드 미디어 그룹 Ilkka-Yhtyma Oyj(헬: ILK...\n", - " ... \n", - "3515 알스트롬 주식거래소 7.2.2.27 10시 30분에 발표 알스트롬 주식회사의 총 5...\n", - "4503 라우트 대변인은 \"다른 인력에 대한 적응 조치는 당분간 적절하다\"고 말했다.\n", - "434 주식수 증가는 2006년 스톡옵션제도에 따라 회사 경영에 부여된 주식선택권에 따른 ...\n", - "3212 개발사업단도 지난해 11월 대비 개발사업단가를 3분의 1가량 낮추겠다고 밝혔다.\n", - "3773 이 건물에는 제품 개발 및 테스트 실험실이 들어설 예정이다.\n", - "Name: kor_sentence, Length: 3876, dtype: object" - ] - }, - "execution_count": 26, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "sentence_train" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [], - "source": [ - "train_encoding = tokenizer(\n", - " sentence_train.tolist(),\n", - " return_tensors='pt',\n", - " padding=True,\n", - " truncation=True\n", - " )\n", - "\n", - "test_encoding = tokenizer(\n", - " sentence_test.tolist(),\n", - " return_tensors='pt',\n", - " padding=True,\n", - " truncation=True\n", - " )" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": {}, - "outputs": [], - "source": [ - "train_set = SentimentalDataset(train_encoding, label_train.reset_index(drop=True))\n", - "test_set = SentimentalDataset(test_encoding, label_test.reset_index(drop=True))" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "metadata": {}, - "outputs": [], - "source": [ - "training_args = TrainingArguments(\n", - " output_dir = './outputs',\n", - " logging_steps = 50,\n", - " num_train_epochs = 1,\n", - " per_device_train_batch_size=32,\n", - " per_device_eval_batch_size=32,\n", - " fp16=True\n", - " )\n", - "\n", - "trainer = Trainer(\n", - " model=model,\n", - " args=training_args,\n", - " train_dataset=train_set,\n", - " eval_dataset=test_set,\n", - " compute_metrics=compute_metrics\n", - " )\n" - ] - }, - { - "cell_type": "code", - "execution_count": 24, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "huggingface/tokenizers: The current process just got forked, after parallelism has already been used. Disabling parallelism to avoid deadlocks...\n", - "To disable this warning, you can either:\n", - "\t- Avoid using `tokenizers` before the fork if possible\n", - "\t- Explicitly set the environment variable TOKENIZERS_PARALLELISM=(true | false)\n", - "/bin/bash: wandb: command not found\n" - ] - } - ], - "source": [ - "!wandb offline" - ] - }, - { - "cell_type": "code", - "execution_count": 25, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "
\n", - " \n", - " \n", - " [ 51/122 00:04 < 00:06, 11.13 it/s, Epoch 0.41/1]\n", - "
\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
StepTraining Loss

" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "ename": "AttributeError", - "evalue": "module 'wandb' has no attribute 'log'", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mAttributeError\u001b[0m Traceback (most recent call last)", - "\u001b[1;32m/opt/ml/level3_nlp_finalproject-nlp-04/experiment/datasets.ipynb Cell 22\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[39mprint\u001b[39m(\u001b[39m'\u001b[39m\u001b[39m---train start---\u001b[39m\u001b[39m'\u001b[39m)\n\u001b[0;32m----> 2\u001b[0m trainer\u001b[39m.\u001b[39;49mtrain()\n", - "File \u001b[0;32m/opt/conda/envs/mrc/lib/python3.10/site-packages/transformers/trainer.py:1664\u001b[0m, in \u001b[0;36mTrainer.train\u001b[0;34m(self, resume_from_checkpoint, trial, ignore_keys_for_eval, **kwargs)\u001b[0m\n\u001b[1;32m 1659\u001b[0m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39mmodel_wrapped \u001b[39m=\u001b[39m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39mmodel\n\u001b[1;32m 1661\u001b[0m inner_training_loop \u001b[39m=\u001b[39m find_executable_batch_size(\n\u001b[1;32m 1662\u001b[0m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39m_inner_training_loop, \u001b[39mself\u001b[39m\u001b[39m.\u001b[39m_train_batch_size, args\u001b[39m.\u001b[39mauto_find_batch_size\n\u001b[1;32m 1663\u001b[0m )\n\u001b[0;32m-> 1664\u001b[0m \u001b[39mreturn\u001b[39;00m inner_training_loop(\n\u001b[1;32m 1665\u001b[0m args\u001b[39m=\u001b[39;49margs,\n\u001b[1;32m 1666\u001b[0m resume_from_checkpoint\u001b[39m=\u001b[39;49mresume_from_checkpoint,\n\u001b[1;32m 1667\u001b[0m trial\u001b[39m=\u001b[39;49mtrial,\n\u001b[1;32m 1668\u001b[0m ignore_keys_for_eval\u001b[39m=\u001b[39;49mignore_keys_for_eval,\n\u001b[1;32m 1669\u001b[0m )\n", - "File \u001b[0;32m/opt/conda/envs/mrc/lib/python3.10/site-packages/transformers/trainer.py:2019\u001b[0m, in \u001b[0;36mTrainer._inner_training_loop\u001b[0;34m(self, batch_size, args, resume_from_checkpoint, trial, ignore_keys_for_eval)\u001b[0m\n\u001b[1;32m 2016\u001b[0m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39mstate\u001b[39m.\u001b[39mepoch \u001b[39m=\u001b[39m epoch \u001b[39m+\u001b[39m (step \u001b[39m+\u001b[39m \u001b[39m1\u001b[39m \u001b[39m+\u001b[39m steps_skipped) \u001b[39m/\u001b[39m steps_in_epoch\n\u001b[1;32m 2017\u001b[0m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39mcontrol \u001b[39m=\u001b[39m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39mcallback_handler\u001b[39m.\u001b[39mon_step_end(args, \u001b[39mself\u001b[39m\u001b[39m.\u001b[39mstate, \u001b[39mself\u001b[39m\u001b[39m.\u001b[39mcontrol)\n\u001b[0;32m-> 2019\u001b[0m \u001b[39mself\u001b[39;49m\u001b[39m.\u001b[39;49m_maybe_log_save_evaluate(tr_loss, model, trial, epoch, ignore_keys_for_eval)\n\u001b[1;32m 2020\u001b[0m \u001b[39melse\u001b[39;00m:\n\u001b[1;32m 2021\u001b[0m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39mcontrol \u001b[39m=\u001b[39m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39mcallback_handler\u001b[39m.\u001b[39mon_substep_end(args, \u001b[39mself\u001b[39m\u001b[39m.\u001b[39mstate, \u001b[39mself\u001b[39m\u001b[39m.\u001b[39mcontrol)\n", - "File \u001b[0;32m/opt/conda/envs/mrc/lib/python3.10/site-packages/transformers/trainer.py:2286\u001b[0m, in \u001b[0;36mTrainer._maybe_log_save_evaluate\u001b[0;34m(self, tr_loss, model, trial, epoch, ignore_keys_for_eval)\u001b[0m\n\u001b[1;32m 2283\u001b[0m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39m_globalstep_last_logged \u001b[39m=\u001b[39m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39mstate\u001b[39m.\u001b[39mglobal_step\n\u001b[1;32m 2284\u001b[0m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39mstore_flos()\n\u001b[0;32m-> 2286\u001b[0m \u001b[39mself\u001b[39;49m\u001b[39m.\u001b[39;49mlog(logs)\n\u001b[1;32m 2288\u001b[0m metrics \u001b[39m=\u001b[39m \u001b[39mNone\u001b[39;00m\n\u001b[1;32m 2289\u001b[0m \u001b[39mif\u001b[39;00m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39mcontrol\u001b[39m.\u001b[39mshould_evaluate:\n", - "File \u001b[0;32m/opt/conda/envs/mrc/lib/python3.10/site-packages/transformers/trainer.py:2648\u001b[0m, in \u001b[0;36mTrainer.log\u001b[0;34m(self, logs)\u001b[0m\n\u001b[1;32m 2646\u001b[0m output \u001b[39m=\u001b[39m {\u001b[39m*\u001b[39m\u001b[39m*\u001b[39mlogs, \u001b[39m*\u001b[39m\u001b[39m*\u001b[39m{\u001b[39m\"\u001b[39m\u001b[39mstep\u001b[39m\u001b[39m\"\u001b[39m: \u001b[39mself\u001b[39m\u001b[39m.\u001b[39mstate\u001b[39m.\u001b[39mglobal_step}}\n\u001b[1;32m 2647\u001b[0m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39mstate\u001b[39m.\u001b[39mlog_history\u001b[39m.\u001b[39mappend(output)\n\u001b[0;32m-> 2648\u001b[0m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39mcontrol \u001b[39m=\u001b[39m \u001b[39mself\u001b[39;49m\u001b[39m.\u001b[39;49mcallback_handler\u001b[39m.\u001b[39;49mon_log(\u001b[39mself\u001b[39;49m\u001b[39m.\u001b[39;49margs, \u001b[39mself\u001b[39;49m\u001b[39m.\u001b[39;49mstate, \u001b[39mself\u001b[39;49m\u001b[39m.\u001b[39;49mcontrol, logs)\n", - "File \u001b[0;32m/opt/conda/envs/mrc/lib/python3.10/site-packages/transformers/trainer_callback.py:390\u001b[0m, in \u001b[0;36mCallbackHandler.on_log\u001b[0;34m(self, args, state, control, logs)\u001b[0m\n\u001b[1;32m 388\u001b[0m \u001b[39mdef\u001b[39;00m \u001b[39mon_log\u001b[39m(\u001b[39mself\u001b[39m, args: TrainingArguments, state: TrainerState, control: TrainerControl, logs):\n\u001b[1;32m 389\u001b[0m control\u001b[39m.\u001b[39mshould_log \u001b[39m=\u001b[39m \u001b[39mFalse\u001b[39;00m\n\u001b[0;32m--> 390\u001b[0m \u001b[39mreturn\u001b[39;00m \u001b[39mself\u001b[39;49m\u001b[39m.\u001b[39;49mcall_event(\u001b[39m\"\u001b[39;49m\u001b[39mon_log\u001b[39;49m\u001b[39m\"\u001b[39;49m, args, state, control, logs\u001b[39m=\u001b[39;49mlogs)\n", - "File \u001b[0;32m/opt/conda/envs/mrc/lib/python3.10/site-packages/transformers/trainer_callback.py:397\u001b[0m, in \u001b[0;36mCallbackHandler.call_event\u001b[0;34m(self, event, args, state, control, **kwargs)\u001b[0m\n\u001b[1;32m 395\u001b[0m \u001b[39mdef\u001b[39;00m \u001b[39mcall_event\u001b[39m(\u001b[39mself\u001b[39m, event, args, state, control, \u001b[39m*\u001b[39m\u001b[39m*\u001b[39mkwargs):\n\u001b[1;32m 396\u001b[0m \u001b[39mfor\u001b[39;00m callback \u001b[39min\u001b[39;00m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39mcallbacks:\n\u001b[0;32m--> 397\u001b[0m result \u001b[39m=\u001b[39m \u001b[39mgetattr\u001b[39;49m(callback, event)(\n\u001b[1;32m 398\u001b[0m args,\n\u001b[1;32m 399\u001b[0m state,\n\u001b[1;32m 400\u001b[0m control,\n\u001b[1;32m 401\u001b[0m model\u001b[39m=\u001b[39;49m\u001b[39mself\u001b[39;49m\u001b[39m.\u001b[39;49mmodel,\n\u001b[1;32m 402\u001b[0m tokenizer\u001b[39m=\u001b[39;49m\u001b[39mself\u001b[39;49m\u001b[39m.\u001b[39;49mtokenizer,\n\u001b[1;32m 403\u001b[0m optimizer\u001b[39m=\u001b[39;49m\u001b[39mself\u001b[39;49m\u001b[39m.\u001b[39;49moptimizer,\n\u001b[1;32m 404\u001b[0m lr_scheduler\u001b[39m=\u001b[39;49m\u001b[39mself\u001b[39;49m\u001b[39m.\u001b[39;49mlr_scheduler,\n\u001b[1;32m 405\u001b[0m train_dataloader\u001b[39m=\u001b[39;49m\u001b[39mself\u001b[39;49m\u001b[39m.\u001b[39;49mtrain_dataloader,\n\u001b[1;32m 406\u001b[0m eval_dataloader\u001b[39m=\u001b[39;49m\u001b[39mself\u001b[39;49m\u001b[39m.\u001b[39;49meval_dataloader,\n\u001b[1;32m 407\u001b[0m \u001b[39m*\u001b[39;49m\u001b[39m*\u001b[39;49mkwargs,\n\u001b[1;32m 408\u001b[0m )\n\u001b[1;32m 409\u001b[0m \u001b[39m# A Callback can skip the return of `control` if it doesn't change it.\u001b[39;00m\n\u001b[1;32m 410\u001b[0m \u001b[39mif\u001b[39;00m result \u001b[39mis\u001b[39;00m \u001b[39mnot\u001b[39;00m \u001b[39mNone\u001b[39;00m:\n", - "File \u001b[0;32m/opt/conda/envs/mrc/lib/python3.10/site-packages/transformers/integrations.py:807\u001b[0m, in \u001b[0;36mWandbCallback.on_log\u001b[0;34m(self, args, state, control, model, logs, **kwargs)\u001b[0m\n\u001b[1;32m 805\u001b[0m \u001b[39mif\u001b[39;00m state\u001b[39m.\u001b[39mis_world_process_zero:\n\u001b[1;32m 806\u001b[0m logs \u001b[39m=\u001b[39m rewrite_logs(logs)\n\u001b[0;32m--> 807\u001b[0m \u001b[39mself\u001b[39;49m\u001b[39m.\u001b[39;49m_wandb\u001b[39m.\u001b[39;49mlog({\u001b[39m*\u001b[39m\u001b[39m*\u001b[39mlogs, \u001b[39m\"\u001b[39m\u001b[39mtrain/global_step\u001b[39m\u001b[39m\"\u001b[39m: state\u001b[39m.\u001b[39mglobal_step})\n", - "\u001b[0;31mAttributeError\u001b[0m: module 'wandb' has no attribute 'log'" - ] - } - ], - "source": [ - "\n", - "print('---train start---')\n", - "trainer.train()" - ] - }, - { - "cell_type": "code", - "execution_count": 26, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "1828 핀란드 식품업계 기업들은 우크라이나가 제공하는 기회에 관심이 많다.\n", - "4809 펜틱+아이넨은 미디어하우스가 제공하는 인터넷 콘텐츠 대부분이 영원히 무료일 수는 없...\n", - "1166 또한 이러한 시장에 대한 6년간의 역사적 분석이 제공됩니다.\n", - "277 루키 총리는 핀란드 사본리나에 위치한 키론살미 해협을 가로지르는 철제 구조물의 인도...\n", - "4670 2009년 8월 3일 핀란드 미디어 그룹 Ilkka-Yhtyma Oyj(헬: ILK...\n", - " ... \n", - "3515 알스트롬 주식거래소 7.2.2.27 10시 30분에 발표 알스트롬 주식회사의 총 5...\n", - "4503 라우트 대변인은 \"다른 인력에 대한 적응 조치는 당분간 적절하다\"고 말했다.\n", - "434 주식수 증가는 2006년 스톡옵션제도에 따라 회사 경영에 부여된 주식선택권에 따른 ...\n", - "3212 개발사업단도 지난해 11월 대비 개발사업단가를 3분의 1가량 낮추겠다고 밝혔다.\n", - "3773 이 건물에는 제품 개발 및 테스트 실험실이 들어설 예정이다.\n", - "Name: kor_sentence, Length: 3876, dtype: object" - ] - }, - "execution_count": 26, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "sentence_train" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "numpy.ndarray" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "type(data)" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "dataset.datasets.SentimentalDataset" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "type(train_set)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "mrc", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.11" - }, - "orig_nbformat": 4, - "vscode": { - "interpreter": { - "hash": "6433bde22504cbf34326cab27df20b94e196fcf98213f776ce9807cc95ec7583" - } - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/Notebooks/glob.ipynb b/Notebooks/glob.ipynb deleted file mode 100644 index 54f9977..0000000 --- a/Notebooks/glob.ipynb +++ /dev/null @@ -1,320 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [], - "source": [ - "import pandas as pd\n", - "import glob\n", - "import re" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 공시번호 기준으로 폴더 나누기" - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "data\n", - "data\n", - "data\n", - "data\n", - "data\n" - ] - } - ], - "source": [ - "import os\n", - "import pandas as pd\n", - "\n", - "def merge_and_move_files(directory_path):\n", - " file_list = os.listdir(directory_path)\n", - "\n", - " # 파일들을 처리하기 위해 딕셔너리를 생성합니다.\n", - " files_by_number = {}\n", - "\n", - " # 파일들을 숫자에 따라 그룹화합니다.\n", - " for file_name in file_list:\n", - " if file_name.startswith(\"preprocessed_data_\"):\n", - " number = file_name.split(\"_\")[2]\n", - " if number not in files_by_number:\n", - " files_by_number[number] = []\n", - " files_by_number[number].append(file_name)\n", - "\n", - " # 각 숫자에 대해 폴더를 만들고 해당 폴더로 파일들을 이동합니다.\n", - " for number, files in files_by_number.items():\n", - " folder_path = os.path.join(directory_path, number)\n", - " os.makedirs(folder_path, exist_ok=True)\n", - "\n", - " # 파일들을 위아래로 합쳐서 새로운 파일로 만듭니다.\n", - " combined_data = pd.concat([pd.read_csv(os.path.join(directory_path, file)) for file in files], axis=0)\n", - "\n", - " # 새로운 파일을 해당 폴더로 이동합니다.\n", - " merged_file_path = os.path.join(folder_path, f\"merged_{number}.csv\")\n", - " combined_data.to_csv(merged_file_path, index=False)\n", - "\n", - " # # 이동한 파일들은 삭제합니다. (선택사항)\n", - " # for file in files:\n", - " # file_path = os.path.join(directory_path, file)\n", - " # os.remove(file_path)\n", - "\n", - "if __name__ == \"__main__\":\n", - " target_directory = '/opt/ml/finance_sentiment_corpus/'\n", - " merge_and_move_files(target_directory)\n" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 회사명 기준으로 파일 나누기" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "import pandas as pd\n", - "\n", - "def merge_and_move_files(directory_path):\n", - " # 디렉토리 내의 파일 리스트를 가져옵니다.\n", - " file_list = os.listdir(directory_path)\n", - "\n", - " # 파일들을 처리하기 위해 딕셔너리를 생성합니다.\n", - " files_by_company = {}\n", - "\n", - " # 파일들을 기업 이름에 따라 그룹화합니다.\n", - " for file_name in file_list:\n", - " # if file_name[0].isdigit():\n", - " # if file_name.startswith(\"preprocessed_data_\"):\n", - " file_path = os.path.join(directory_path, file_name)\n", - " data = pd.read_csv(file_path)\n", - " company_name = data['company'].iloc[0]\n", - " if company_name not in files_by_company:\n", - " files_by_company[company_name] = []\n", - " files_by_company[company_name].append(file_name)\n", - "\n", - " # 각 기업에 대해 폴더를 만들고 해당 폴더로 파일들을 이동합니다.\n", - " for company_name, files in files_by_company.items():\n", - " folder_path = os.path.join(directory_path, \"merged\")\n", - " os.makedirs(folder_path, exist_ok=True)\n", - "\n", - " # 파일들을 위아래로 합쳐서 새로운 파일로 만듭니다.\n", - " combined_data = pd.concat([pd.read_csv(os.path.join(directory_path, file)) for file in files], axis=0)\n", - "\n", - " # 새로운 파일을 해당 폴더로 이동합니다.\n", - " merged_file_path = os.path.join(folder_path, f\"merged_{company_name}.csv\")\n", - " combined_data.to_csv(merged_file_path, index=False)\n", - "\n", - " # # 이동한 파일들은 삭제합니다. (선택사항)\n", - " # for file in files:\n", - " # file_path = os.path.join(directory_path, file)\n", - " # os.remove(file_path)\n", - "\n", - "if __name__ == \"__main__\":\n", - " # target_directory = '/opt/ml/finance_sentiment_corpus/'\n", - " target_directory = \"/opt/ml/finance_sentiment_corpus\"\n", - " merge_and_move_files(target_directory)\n" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 파일 합치기" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def merge_csv_files(directory_path, output_file_name):\n", - " file_list = os.listdir(directory_path)\n", - " combined_data = pd.DataFrame()\n", - "\n", - " for file_name in file_list:\n", - " if file_name.endswith(\".csv\") and file_name != \"merged_all.csv\":\n", - " file_path = os.path.join(directory_path, file_name)\n", - " print(file_path)\n", - " data = pd.read_csv(file_path)\n", - " \n", - " if \"label\" in data.columns and \"labels\" not in data.columns :\n", - " data[\"labels\"] = data[\"label\"]\n", - " \n", - " data = data[[\"company\", \"title\", \"date\", \"content_corpus\", \"labels\"]]\n", - " combined_data = pd.concat([combined_data, data], axis=0, ignore_index=True)\n", - " \n", - " output_file_path = os.path.join(directory_path, output_file_name)\n", - " # print(output_file_path)\n", - " if os.path.exists(output_file_path):\n", - " os.remove(output_file_path)\n", - " \n", - " combined_data.to_csv(output_file_path, index=False)\n", - " return combined_data\n", - "\n", - "directory_path = \"/opt/ml/finance_sentiment_corpus/merged\"\n", - "output_file_name = \"merged_all.csv\"\n", - "output_file_path = directory_path + output_file_name\n", - " \n", - "data = merge_csv_files(directory_path , output_file_name)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 전처리해서 chat gpt한테 넣어주기" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "/opt/ml/finance_sentiment_corpus/merged/merged_LG전자.csv\n", - "/opt/ml/finance_sentiment_corpus/merged/merged_KT&G.csv\n", - "/opt/ml/finance_sentiment_corpus/merged/merged_셀트리온.csv\n", - "/opt/ml/finance_sentiment_corpus/merged/merged_현대모비스.csv\n", - "/opt/ml/finance_sentiment_corpus/merged/merged_LG.csv\n", - "/opt/ml/finance_sentiment_corpus/merged/merged_SK이노베이션.csv\n", - "/opt/ml/finance_sentiment_corpus/merged/merged_카카오뱅크.csv\n", - "/opt/ml/finance_sentiment_corpus/merged/merged_HD현대중공업.csv\n", - "/opt/ml/finance_sentiment_corpus/merged/merged_한국전력.csv\n", - "/opt/ml/finance_sentiment_corpus/merged/merged_POSCO홀딩스.csv\n", - "/opt/ml/finance_sentiment_corpus/merged/merged_두산에너빌리티.csv\n", - "/opt/ml/finance_sentiment_corpus/merged/merged_삼성바이오로직스.csv\n", - "/opt/ml/finance_sentiment_corpus/merged/merged_LG화학.csv\n", - "/opt/ml/finance_sentiment_corpus/merged/merged_에코프로비엠.csv\n", - "/opt/ml/finance_sentiment_corpus/merged/merged_삼성물산.csv\n", - "/opt/ml/finance_sentiment_corpus/merged/merged_SK하이닉스.csv\n", - "/opt/ml/finance_sentiment_corpus/merged/merged_삼성SDI.csv\n", - "/opt/ml/finance_sentiment_corpus/merged/merged_삼성화재.csv\n", - "/opt/ml/finance_sentiment_corpus/merged/merged_카카오.csv\n", - "/opt/ml/finance_sentiment_corpus/merged/merged_포스코퓨처엠.csv\n", - "/opt/ml/finance_sentiment_corpus/merged/merged_기아.csv\n", - "/opt/ml/finance_sentiment_corpus/merged/merged_하이브.csv\n", - "/opt/ml/finance_sentiment_corpus/merged/merged_삼성생명.csv\n", - "/opt/ml/finance_sentiment_corpus/merged/merged_에코프로.csv\n", - "/opt/ml/finance_sentiment_corpus/merged/merged_신한지주.csv\n", - "/opt/ml/finance_sentiment_corpus/merged/merged_KB금융.csv\n" - ] - } - ], - "source": [ - "def merge_csv_files(directory_path, output_file_name):\n", - " file_list = os.listdir(directory_path)\n", - " combined_data = pd.DataFrame()\n", - "\n", - " for file_name in file_list:\n", - " if file_name.endswith(\".csv\") and file_name != \"merged_all.csv\":\n", - " file_path = os.path.join(directory_path, file_name)\n", - " print(file_path)\n", - " data = pd.read_csv(file_path)\n", - " \n", - " # if \"label\" in data.columns and \"labels\" not in data.columns :\n", - " # data[\"labels\"] = data[\"label\"]\n", - " \n", - " data = data[[\"company\", \"title\", \"date\", \"content_corpus\"]]\n", - " combined_data = pd.concat([combined_data, data], axis=0, ignore_index=True)\n", - " \n", - " output_file_path = os.path.join(directory_path, output_file_name)\n", - " # print(output_file_path)\n", - " if os.path.exists(output_file_path):\n", - " os.remove(output_file_path)\n", - " \n", - " combined_data.to_csv(output_file_path, index=False)\n", - " return combined_data\n", - "\n", - "directory_path = \"/opt/ml/finance_sentiment_corpus/merged\"\n", - "output_file_name = \"merged_all.csv\"\n", - "output_file_path = directory_path + output_file_name\n", - " \n", - "data = merge_csv_files(directory_path , output_file_name)" - ] - }, - { - "cell_type": "code", - "execution_count": 57, - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "import pandas as pd\n", - "\n", - "def merge_csv_files(directory_path, output_file_name):\n", - " file_list = os.listdir(directory_path)\n", - " combined_data = pd.DataFrame()\n", - "\n", - " for file_name in file_list:\n", - " if file_name.endswith(\".csv\"):\n", - " file_path = os.path.join(directory_path, file_name)\n", - " data = pd.read_csv(file_path)\n", - " combined_data = pd.concat([combined_data, data], axis=0, ignore_index=True)\n", - "\n", - " output_file_path = os.path.join(directory_path, output_file_name)\n", - "\n", - " combined_data.to_csv(output_file_path, index=False)\n", - "\n", - "directory_path = \"/opt/ml/finance_sentiment_corpus/\"\n", - "output_file_name = \"merged_all.csv\"\n", - "output_file_path = directory_path + output_file_name\n", - "\n", - "if os.path.exists(output_file_path):\n", - " os.remove(output_file_path)\n", - "# merge_csv_files(\"/opt/ml/finance_sentiment_corpus\", \"merged_all.csv\")" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "final", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8.5" - }, - "orig_nbformat": 4, - "vscode": { - "interpreter": { - "hash": "981f108a204f421f158e0977940335d851edffa6dd3586828a3e1aec045160e4" - } - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/Notebooks/train.ipynb b/Notebooks/train.ipynb deleted file mode 100644 index f992c70..0000000 --- a/Notebooks/train.ipynb +++ /dev/null @@ -1,1011 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/opt/conda/envs/final/lib/python3.8/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", - " from .autonotebook import tqdm as notebook_tqdm\n" - ] - } - ], - "source": [ - "import torch\n", - "import pandas as pd\n", - "import sklearn\n", - "import random\n", - "import numpy as np\n", - "import wandb\n", - "\n", - "from transformers import AutoModel\n", - "from transformers import AutoModelForSequenceClassification, AutoTokenizer, BertForSequenceClassification\n", - "from transformers import TrainingArguments, Trainer\n", - "from transformers import pipeline\n", - "from transformers import DebertaV2ForSequenceClassification\n", - "from transformers import RobertaTokenizer, RobertaForSequenceClassification\n", - "\n", - "from dataset.datasets import SentimentalDataset\n", - "from metrics.metrics import compute_metrics\n", - "\n", - "from sklearn.datasets import load_iris # 샘플 데이터 로딩\n", - "from sklearn.model_selection import train_test_split\n", - "\n", - "from utils.utils import config_seed\n", - "\n", - "import json\n", - "import re" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 설정" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "SEED = 42\n", - "config_seed(SEED)\n", - "device = torch.device(\"cuda\" if torch.cuda.is_available() else 'cpu')" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### 모델 및 토크나이저" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Some weights of the model checkpoint at klue/roberta-large were not used when initializing RobertaForSequenceClassification: ['lm_head.layer_norm.bias', 'lm_head.dense.bias', 'lm_head.bias', 'lm_head.layer_norm.weight', 'lm_head.dense.weight']\n", - "- This IS expected if you are initializing RobertaForSequenceClassification from the checkpoint of a model trained on another task or with another architecture (e.g. initializing a BertForSequenceClassification model from a BertForPreTraining model).\n", - "- This IS NOT expected if you are initializing RobertaForSequenceClassification from the checkpoint of a model that you expect to be exactly identical (initializing a BertForSequenceClassification model from a BertForSequenceClassification model).\n", - "Some weights of RobertaForSequenceClassification were not initialized from the model checkpoint at klue/roberta-large and are newly initialized: ['classifier.dense.bias', 'classifier.dense.weight', 'classifier.out_proj.weight', 'classifier.out_proj.bias']\n", - "You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference.\n" - ] - }, - { - "data": { - "text/plain": [ - "Embedding(32002, 1024)" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "MODEL_NAME = 'klue/roberta-large'\n", - "model = AutoModelForSequenceClassification.from_pretrained(MODEL_NAME, num_labels=2).to(device)\n", - "tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)\n", - "\n", - "special_tokens_dict = {'additional_special_tokens': ['[COMPANY]','[/COMPANY]']}\n", - "num_added_toks = tokenizer.add_special_tokens(special_tokens_dict)\n", - "model.resize_token_embeddings(len(tokenizer))" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [], - "source": [ - "# MODEL_NAME = \"team-lucid/deberta-v3-base-korean\"\n", - "\n", - "# model = DebertaV2ForSequenceClassification.from_pretrained(MODEL_NAME, num_labels=2).to(device)\n", - "# tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 데이터셋 구성" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### 1) 기사 전체 학습" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [], - "source": [ - "# data = pd.read_csv('/opt/ml/finance_sentiment_corpus/merged_samsung_filtered.csv')\n", - "\n", - "# def extract_label(json_str) :\n", - "# data_dict = eval(json_str) # JSON 문자열을 파이썬 딕셔너리로 변환\n", - "# return data_dict[\"label\"]\n", - "\n", - "# # \"label\" 값을 추출하여 새로운 Series 생성\n", - "# data['labels'] = data[\"labels\"].apply(extract_label)\n", - "# data['labels'] = data['labels'].map({'부정':0, '긍정':1})" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### 2) 기사 앞뒤 학습" - ] - }, - { - "cell_type": "code", - "execution_count": 122, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "

\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
companytitledatecontent_corpuslabels
0삼성전자데이터센터 전력 40% 차지하는 D램… 삼성·SK하이닉스, ‘전성비’ ...2023.07.10 15:29챗GPT 시대 화두로 떠오른 전력효율성 문제 ”전력 먹는 하마, D램 전력효율성 개...{\"label\": \"긍정\", \"reason\": \"전력효율성 개선을 위한 솔루션 개발...
1삼성전자“삼성전자가 식품도 팔았어?”…신규 가입 일단 종료한 사연2023.07.10 15:07삼성 가전제품 구매고객에 삼성닷컴 내 e-식품관에서 할인혜택 주며 ‘락인’ 기대 ...{\"label\": \"부정\", \"reason\": \"삼성전자 멤버십 플랜 신규 고객 모...
2삼성전자SGC솔루션 '도어글라스'…삼성·LG 세탁기·건조기에 공급2023.07.10 15:05해외 가전 브랜드 공략…B2B 사업 확장 SGC솔루션 논산 공장. . 생활유리...{\"label\": \"긍정\", \"reason\": \"해외 가전 브랜드들을 공략하며 전 ...
3삼성전자‘페이커’ 내세운 삼성 OLED 게이밍 모니터 글로벌 3천대 돌파2023.07.10 14:58북미·유럽 등 예약 판매 3000대 돌파 10일 오후 6시 삼성닷컴 ‘페이커’ 출연...{\"label\": \"긍정\", \"reason\": \"오디세이 OLED G9의 글로벌 예...
4삼성전자네이처 게재 등 성과…삼성휴먼테크논문대상, 30년 맞았다2023.07.10 14:4829년간 3만6558편 논문 접수…수상자 5312명 9월1일부터 올해 대상 접수…상...{\"label\": \"긍정\", \"reason\": \"삼성휴먼테크논문대상이 올해로 시행 ...
\n", - "
" - ], - "text/plain": [ - " company title date \\\n", - "0 삼성전자 데이터센터 전력 40% 차지하는 D램… 삼성·SK하이닉스, ‘전성비’ ... 2023.07.10 15:29 \n", - "1 삼성전자 “삼성전자가 식품도 팔았어?”…신규 가입 일단 종료한 사연 2023.07.10 15:07 \n", - "2 삼성전자 SGC솔루션 '도어글라스'…삼성·LG 세탁기·건조기에 공급 2023.07.10 15:05 \n", - "3 삼성전자 ‘페이커’ 내세운 삼성 OLED 게이밍 모니터 글로벌 3천대 돌파 2023.07.10 14:58 \n", - "4 삼성전자 네이처 게재 등 성과…삼성휴먼테크논문대상, 30년 맞았다 2023.07.10 14:48 \n", - "\n", - " content_corpus \\\n", - "0 챗GPT 시대 화두로 떠오른 전력효율성 문제 ”전력 먹는 하마, D램 전력효율성 개... \n", - "1 삼성 가전제품 구매고객에 삼성닷컴 내 e-식품관에서 할인혜택 주며 ‘락인’ 기대 ... \n", - "2 해외 가전 브랜드 공략…B2B 사업 확장 SGC솔루션 논산 공장. . 생활유리... \n", - "3 북미·유럽 등 예약 판매 3000대 돌파 10일 오후 6시 삼성닷컴 ‘페이커’ 출연... \n", - "4 29년간 3만6558편 논문 접수…수상자 5312명 9월1일부터 올해 대상 접수…상... \n", - "\n", - " labels \n", - "0 {\"label\": \"긍정\", \"reason\": \"전력효율성 개선을 위한 솔루션 개발... \n", - "1 {\"label\": \"부정\", \"reason\": \"삼성전자 멤버십 플랜 신규 고객 모... \n", - "2 {\"label\": \"긍정\", \"reason\": \"해외 가전 브랜드들을 공략하며 전 ... \n", - "3 {\"label\": \"긍정\", \"reason\": \"오디세이 OLED G9의 글로벌 예... \n", - "4 {\"label\": \"긍정\", \"reason\": \"삼성휴먼테크논문대상이 올해로 시행 ... " - ] - }, - "execution_count": 122, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "data = pd.read_csv(\"/opt/ml/finance_sentiment_corpus/merged/merged_all.csv\")\n", - "data.head()" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "(3778, 5)\n", - "(3777, 5)\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
titledatecontent_corpuslabelscontent_corpus_company
0데이터센터 전력 40% 차지하는 D램… 삼성·SK하이닉스, ‘전성비’ ...2023.07.10 15:29챗GPT 시대 화두로 떠오른 전력효율성 문제 ”전력 먹는 하마, D램 전력효율성 개...1.0이 기사는 [COMPANY]삼성전자[/COMPANY]에 대한 기사. [SEP]. 데...
1“삼성전자가 식품도 팔았어?”…신규 가입 일단 종료한 사연2023.07.10 15:07삼성 가전제품 구매고객에 삼성닷컴 내 e-식품관에서 할인혜택 주며 ‘락인’ 기대 ...0.0이 기사는 [COMPANY]삼성전자[/COMPANY]에 대한 기사. [SEP]. “...
2SGC솔루션 '도어글라스'…삼성·LG 세탁기·건조기에 공급2023.07.10 15:05해외 가전 브랜드 공략…B2B 사업 확장 SGC솔루션 논산 공장. . 생활유리...1.0이 기사는 [COMPANY]삼성전자[/COMPANY]에 대한 기사. [SEP]. S...
3‘페이커’ 내세운 삼성 OLED 게이밍 모니터 글로벌 3천대 돌파2023.07.10 14:58북미·유럽 등 예약 판매 3000대 돌파 10일 오후 6시 삼성닷컴 ‘페이커’ 출연...1.0이 기사는 [COMPANY]삼성전자[/COMPANY]에 대한 기사. [SEP]. ‘...
4네이처 게재 등 성과…삼성휴먼테크논문대상, 30년 맞았다2023.07.10 14:4829년간 3만6558편 논문 접수…수상자 5312명 9월1일부터 올해 대상 접수…상...1.0이 기사는 [COMPANY]삼성전자[/COMPANY]에 대한 기사. [SEP]. 네...
\n", - "
" - ], - "text/plain": [ - " title date \\\n", - "0 데이터센터 전력 40% 차지하는 D램… 삼성·SK하이닉스, ‘전성비’ ... 2023.07.10 15:29 \n", - "1 “삼성전자가 식품도 팔았어?”…신규 가입 일단 종료한 사연 2023.07.10 15:07 \n", - "2 SGC솔루션 '도어글라스'…삼성·LG 세탁기·건조기에 공급 2023.07.10 15:05 \n", - "3 ‘페이커’ 내세운 삼성 OLED 게이밍 모니터 글로벌 3천대 돌파 2023.07.10 14:58 \n", - "4 네이처 게재 등 성과…삼성휴먼테크논문대상, 30년 맞았다 2023.07.10 14:48 \n", - "\n", - " content_corpus labels \\\n", - "0 챗GPT 시대 화두로 떠오른 전력효율성 문제 ”전력 먹는 하마, D램 전력효율성 개... 1.0 \n", - "1 삼성 가전제품 구매고객에 삼성닷컴 내 e-식품관에서 할인혜택 주며 ‘락인’ 기대 ... 0.0 \n", - "2 해외 가전 브랜드 공략…B2B 사업 확장 SGC솔루션 논산 공장. . 생활유리... 1.0 \n", - "3 북미·유럽 등 예약 판매 3000대 돌파 10일 오후 6시 삼성닷컴 ‘페이커’ 출연... 1.0 \n", - "4 29년간 3만6558편 논문 접수…수상자 5312명 9월1일부터 올해 대상 접수…상... 1.0 \n", - "\n", - " content_corpus_company \n", - "0 이 기사는 [COMPANY]삼성전자[/COMPANY]에 대한 기사. [SEP]. 데... \n", - "1 이 기사는 [COMPANY]삼성전자[/COMPANY]에 대한 기사. [SEP]. “... \n", - "2 이 기사는 [COMPANY]삼성전자[/COMPANY]에 대한 기사. [SEP]. S... \n", - "3 이 기사는 [COMPANY]삼성전자[/COMPANY]에 대한 기사. [SEP]. ‘... \n", - "4 이 기사는 [COMPANY]삼성전자[/COMPANY]에 대한 기사. [SEP]. 네... " - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "data = pd.read_csv(\"/opt/ml/finance_sentiment_corpus/merged/merged_all.csv\")\n", - "# data = pd.read_csv(\"/opt/ml/finance_sentiment_corpus/merged/merged_NAVER.csv\")\n", - "\n", - "def remove_idx_row(data) : \n", - " patterns = [r'idx\\s*:?\\s*.+?', r'라벨링\\s*:?\\s*.+?']\n", - " \n", - " for pattern in patterns :\n", - " mask = data['labels'].str.match(pattern)\n", - " data = data.drop(data[mask].index)\n", - "\n", - " return data\n", - " \n", - "# title과 content_corpus에서 원하는 문장 추출\n", - "def extract_sentences(text):\n", - " sentences = text.split('. ')\n", - " if len(sentences) >= 5 :\n", - " return '. '.join([sentences[0], sentences[1], sentences[-2], sentences[-1]])\n", - " else :\n", - " return '. '+text\n", - " \n", - "def extract_label(json_str) :\n", - " json_str = json_str.replace(\"'\", \"\\\"\")\n", - " try:\n", - " json_data = json.loads(json_str)\n", - "\n", - " except json.JSONDecodeError as e:\n", - " if json_str[-2:] == '.}' :\n", - " json_str = json_str[:-2] + \".\\\"}\"\n", - " elif json_str[-1] == \"\\\"\" :\n", - " json_str = json_str + \"}\"\n", - " else:\n", - " json_str += \"\\\"}\"\n", - " \n", - " try:\n", - " data_dict = json.loads(json_str)\n", - " except json.JSONDecodeError as e:\n", - " return None\n", - "\n", - " return data_dict[\"label\"]\n", - " \n", - "def preprocessing_label(json_str) :\n", - " json_str = re.sub(r\"^.*### 출력\\s?:?\\s?\\n?\\s?\", \"\", str(json_str))\n", - " # json_str= json_str.replace(\"\\\"\", \"'\")\n", - " json_str = json_str.replace(\"'\", \"\\\\'\") # python interpreter에서 한번, json에서 한번\n", - " \n", - " return json_str\n", - "\n", - "data = remove_idx_row(data)\n", - "\n", - "# \"label\" column만 있다면 바꾸자.\n", - "if \"label\" in data.columns and \"labels\" not in data.columns :\n", - " data[\"labels\"] = data[\"label\"]\n", - "\n", - "data[\"labels\"] = data[\"labels\"].apply(preprocessing_label)\n", - "data['labels'] = data[\"labels\"].apply(extract_label)\n", - "data['content_corpus_company'] = data.apply(lambda row: '이 기사는 [COMPANY]'+ str(row[\"company\"]) +'[/COMPANY]에 대한 기사. [SEP]'+ extract_sentences(row['title']) + ' ' + extract_sentences(row['content_corpus']), axis=1)\n", - "data['labels'] = data['labels'].map({'부정':0, '긍정':1})\n", - "data = data[[\"title\", \"date\", \"content_corpus\", \"labels\", \"content_corpus_company\"]]\n", - "print(data.shape)\n", - "data = data[data['labels'].notna()]\n", - "print(data.shape)\n", - "data.head()" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Cross validation" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [], - "source": [ - "# # dataset = train_test_split(data['content_corpus'], data['labels'],\n", - "\n", - "# # train_dataset, test_dataset = train_test_split(data['new_column'], data['labels'],\n", - "# # test_size=0.2, shuffle=True, stratify=data['labels'], # label에 비율을 맞춰서 분리\n", - "# # random_state=SEED)\n", - "\n", - "# train_dataset, test_dataset = train_test_split(data,\n", - "# test_size=0.3, shuffle=True, stratify=data['labels'], # label에 비율을 맞춰서 분리\n", - "# random_state=SEED)\n", - "\n", - "# train_dataset, val_dataset = train_test_split(train_dataset,\n", - "# test_size=0.2, shuffle=True, stratify=train_dataset['labels'], # label에 비율을 맞춰서 분리\n", - "# random_state=SEED)\n", - "\n", - "# corpus_train, label_train = train_dataset[\"new_column\"], train_dataset[\"labels\"]\n", - "# corpus_val, label_val = val_dataset[\"new_column\"], val_dataset[\"labels\"]\n", - "# corpus_test, label_test = test_dataset[\"new_column\"], test_dataset[\"labels\"]\n", - "\n", - "# # sentence_train, sentence_val, label_train, label_val = dataset\n", - "\n", - "\n", - "# max_length=40\n", - "# stride=10\n", - "# ## TODO 임의의 값으로 차후 수정\n", - "# train_encoding = tokenizer(corpus_train.tolist(), ## pandas.Series -> list\n", - "# return_tensors='pt',\n", - "# padding=True,\n", - "# truncation=True,\n", - "# ##\n", - "# max_length=max_length,\n", - "# stride=stride,\n", - "# return_overflowing_tokens=True,\n", - "# return_offsets_mapping=False\n", - "# )\n", - "\n", - "# val_encoding = tokenizer(corpus_val.tolist(),\n", - "# return_tensors='pt',\n", - "# padding=True,\n", - "# truncation=True,\n", - "# ##\n", - "# max_length=max_length,\n", - "# stride=stride,\n", - "# return_overflowing_tokens=True,\n", - "# return_offsets_mapping=False\n", - "# )\n", - "\n", - "# train_set = SentimentalDataset(train_encoding, label_train.reset_index(drop=True))\n", - "# val_set = SentimentalDataset(val_encoding, label_val.reset_index(drop=True))\n", - "# test_set = SentimentalDataset(val_encoding, label_test.reset_index(drop=True))" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [], - "source": [ - "dataset = train_test_split(data['content_corpus_company'], data['labels'],\n", - " test_size=0.2, shuffle=True, stratify=data['labels'], # label에 비율을 맞춰서 분리\n", - " random_state=SEED)\n", - "\n", - "\n", - "sentence_train, sentence_val, label_train, label_val = dataset\n", - "\n", - "\n", - "max_length=500\n", - "# max_length = 2000\n", - "stride=10\n", - "## TODO 임의의 값으로 차후 수정\n", - "train_encoding = tokenizer(sentence_train.tolist(), ## pandas.Series -> list\n", - " return_tensors='pt',\n", - " padding=True,\n", - " truncation=True,\n", - " ##\n", - " max_length=max_length,\n", - " stride=stride,\n", - " # return_overflowing_tokens=True,\n", - " return_offsets_mapping=False\n", - " )\n", - "\n", - "val_encoding = tokenizer(sentence_val.tolist(),\n", - " return_tensors='pt',\n", - " padding=True,\n", - " truncation=True,\n", - " ##\n", - " max_length=max_length,\n", - " stride=stride,\n", - " # return_overflowing_tokens=True,\n", - " return_offsets_mapping=False\n", - " )\n", - "\n", - "train_set = SentimentalDataset(train_encoding, label_train.reset_index(drop=True))\n", - "val_set = SentimentalDataset(val_encoding, label_val.reset_index(drop=True))" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 학습 (huggingface)\n", - "#### hyperparameter\n", - "- max_length\n", - "- stride\n", - "- num_train_epoch\n", - "- learning_rate\n", - "- per_device_train_batch_size\n", - "- per_device_eval_batch_size" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [], - "source": [ - "logging_steps = 200\n", - "num_train_epochs = 3\n", - "per_device_train_batch_size = 4\n", - "per_device_eval_batch_size = 4\n", - "learning_rate = 5e-6" - ] - }, - { - "cell_type": "code", - "execution_count": 57, - "metadata": {}, - "outputs": [], - "source": [ - "# !wandb online" - ] - }, - { - "cell_type": "code", - "execution_count": 131, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "
\n", - " \n", - " \n", - " [2271/2271 08:54, Epoch 3/3]\n", - "
\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
EpochTraining LossValidation LossAccuracyMicro F1Macro F1
10.3565000.3422700.9128140.9128140.893988
20.2326000.3953610.9180980.9180980.895991
30.0999000.4616850.9194190.9194190.897826

" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "TrainOutput(global_step=2271, training_loss=0.2475055377948447, metrics={'train_runtime': 534.9292, 'train_samples_per_second': 16.982, 'train_steps_per_second': 4.245, 'total_flos': 8267250492648000.0, 'train_loss': 0.2475055377948447, 'epoch': 3.0})" - ] - }, - "execution_count": 131, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# run = wandb.init(project=\"final_sentimental\", entity=\"nlp-10\")\n", - "\n", - "# run.name = f\"model: {MODEL_NAME} / batch_size: {per_device_train_batch_size} / lr: {learning_rate}\"\n", - "\n", - "training_args = TrainingArguments(\n", - " output_dir = './outputs',\n", - " logging_steps = logging_steps,\n", - " num_train_epochs = num_train_epochs,\n", - " per_device_train_batch_size = per_device_train_batch_size,\n", - " per_device_eval_batch_size = per_device_eval_batch_size,\n", - " learning_rate = learning_rate,\n", - " evaluation_strategy=\"epoch\", \n", - " fp16=True,\n", - " report_to=\"wandb\",\n", - ")\n", - "\n", - "trainer = Trainer(\n", - " model=model,\n", - " args=training_args,\n", - " train_dataset=train_set,\n", - " eval_dataset=val_set,\n", - " compute_metrics=compute_metrics\n", - ")\n", - "\n", - "print('---train start---')\n", - "trainer.train()\n", - "# wandb.finish()" - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "metadata": {}, - "outputs": [], - "source": [ - "# torch.save(model, \"/opt/ml/input/model-roberta_large-sota\")\n", - "trainer.save_model(\"/opt/ml/input/model-roberta_large-sota_trainer_company-name\") " - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "huggingface/tokenizers: The current process just got forked, after parallelism has already been used. Disabling parallelism to avoid deadlocks...\n", - "To disable this warning, you can either:\n", - "\t- Avoid using `tokenizers` before the fork if possible\n", - "\t- Explicitly set the environment variable TOKENIZERS_PARALLELISM=(true | false)\n", - "Sat Jul 22 12:44:18 2023 \n", - "+-----------------------------------------------------------------------------+\n", - "| NVIDIA-SMI 450.80.02 Driver Version: 450.80.02 CUDA Version: 11.0 |\n", - "|-------------------------------+----------------------+----------------------+\n", - "| GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC |\n", - "| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |\n", - "| | | MIG M. |\n", - "|===============================+======================+======================|\n", - "| 0 Tesla V100-PCIE... On | 00000000:00:05.0 Off | Off |\n", - "| N/A 41C P0 44W / 250W | 32455MiB / 32510MiB | 0% Default |\n", - "| | | N/A |\n", - "+-------------------------------+----------------------+----------------------+\n", - " \n", - "+-----------------------------------------------------------------------------+\n", - "| Processes: |\n", - "| GPU GI CI PID Type Process name GPU Memory |\n", - "| ID ID Usage |\n", - "|=============================================================================|\n", - "+-----------------------------------------------------------------------------+\n" - ] - } - ], - "source": [ - "!nvidia-smi" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [], - "source": [ - "import torch, gc\n", - "gc.collect()\n", - "torch.cuda.empty_cache()" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 평가" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# run.finish()" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "---val evaulate start---\n" - ] - }, - { - "ename": "NameError", - "evalue": "name 'model' is not defined", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", - "\u001b[1;32m/opt/ml/level3_nlp_finalproject-nlp-04/sentence-sentimental/train.ipynb Cell 24\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[39mprint\u001b[39m(\u001b[39m'\u001b[39m\u001b[39m---val evaulate start---\u001b[39m\u001b[39m'\u001b[39m)\n\u001b[1;32m 2\u001b[0m \u001b[39m# wandb.init()\u001b[39;00m\n\u001b[1;32m 3\u001b[0m \u001b[39m# trainer.evaluate(eval_dataset=val_set, metric_key_prefix='val1')\u001b[39;00m\n\u001b[0;32m----> 4\u001b[0m model\u001b[39m.\u001b[39mevaluate(eval_dataset\u001b[39m=\u001b[39mval_set, metric_key_prefix\u001b[39m=\u001b[39m\u001b[39m'\u001b[39m\u001b[39mval1\u001b[39m\u001b[39m'\u001b[39m)\n\u001b[1;32m 5\u001b[0m \u001b[39m# wandb.finish()\u001b[39;00m\n", - "\u001b[0;31mNameError\u001b[0m: name 'model' is not defined" - ] - } - ], - "source": [ - "print('---val evaulate start---')\n", - "# wandb.init()\n", - "# trainer.evaluate(eval_dataset=val_set, metric_key_prefix='val1')\n", - "# wandb.finish()" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 평가" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "

\n", - " \n", - " \n", - " [303/303 00:10]\n", - "
\n", - " " - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/opt/ml/level3_nlp_finalproject-nlp-04/sentence-sentimental/metrics/metrics.py:7: FutureWarning: load_metric is deprecated and will be removed in the next major version of datasets. Use 'evaluate.load' instead, from the new library 🤗 Evaluate: https://huggingface.co/docs/evaluate\n", - " acc = load_metric('accuracy').compute(predictions=preds, references=labels)['accuracy']\n" - ] - }, - { - "data": { - "text/plain": [ - "{'val1_loss': 0.5156943798065186,\n", - " 'val1_accuracy': 0.8489475856376393,\n", - " 'val1_f1': 0.8489475856376393,\n", - " 'val1_runtime': 12.6519,\n", - " 'val1_samples_per_second': 191.513,\n", - " 'val1_steps_per_second': 23.949,\n", - " 'epoch': 2.0}" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "trainer.evaluate(eval_dataset=val_set, metric_key_prefix='val1')" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### inference" - ] - }, - { - "cell_type": "code", - "execution_count": 43, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "---inference start---\n", - "######################################\n", - "tensor([[ 3.0807, -3.1782]])\n", - "tensor([0.9981, 0.0019])\n" - ] - }, - { - "data": { - "text/plain": [ - "tensor([0.9981, 0.0019])" - ] - }, - "execution_count": 43, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "print('---inference start---')\n", - "text = '이 기사는 [COMPANY]'+ '삼성전자' +'[/COMPANY]에 대한 기사. [SEP] \"\n", - "corpus = \"삼성전자가 테슬라와 4000억 규모의 협약이 미뤄졌다.\"\n", - "# MODEL_PATH = \"/opt/ml/input/model-roberta_large-sota_trainer\"\n", - "MODEL_PATH = \"/opt/ml/input/model-roberta_large-sota_trainer_company-name\"\n", - "tokenizer = AutoTokenizer.from_pretrained(\"klue/roberta-large\")\n", - "model = AutoModelForSequenceClassification.from_pretrained(MODEL_PATH)\n", - "\n", - "\n", - "# # model = torch.load(PATH)\n", - "model.eval()\n", - "with torch.no_grad() :\n", - " temp = tokenizer(\n", - " my_text,\n", - " return_tensors='pt',\n", - " padding=True,\n", - " truncation=True,\n", - " ##\n", - " max_length=100,\n", - " # stride=stride,\n", - " return_overflowing_tokens=True,\n", - " return_offsets_mapping=False\n", - " )\n", - "\n", - " \n", - " temp = {\n", - " 'input_ids':temp['input_ids'],\n", - " 'token_type_ids':temp['token_type_ids'],\n", - " 'attention_mask':temp['attention_mask'],\n", - " }\n", - " # print(temp)\n", - " \n", - " print(\"######################################\")\n", - " predicted_label = model(temp['input_ids'])\n", - " print(predicted_label.logits)\n", - " print(torch.nn.Softmax(dim=-1)(predicted_label.logits).mean(dim=0))\n", - " [20 80] => [50]\n", - " [[20, 80], [30, 70]]\n", - " \n", - "\n", - "torch.nn.Softmax(dim=-1)(predicted_label.logits).mean(dim=0)\n", - " " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "## 위에 결과에서 앞의 것이 부정 뒤에것이 긍정" - ] - }, - { - "cell_type": "code", - "execution_count": 37, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "부정\n" - ] - } - ], - "source": [ - "result = torch.nn.Softmax(dim=-1)(predicted_label.logits).mean(dim=0)\n", - "\n", - "if result[0] > result[1] :\n", - " print(\"부정\")\n", - "else :\n", - " print(\"긍정\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "final", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8.5" - }, - "orig_nbformat": 4, - "vscode": { - "interpreter": { - "hash": "981f108a204f421f158e0977940335d851edffa6dd3586828a3e1aec045160e4" - } - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/README.md b/README.md index b4be4b4..b283871 100644 --- a/README.md +++ b/README.md @@ -55,26 +55,45 @@ - 기사 전체에서 단어의 가중치를 계산하고, 해당 가중치를 이용한 주요 단어 후보를 선정합니다. - 한국어 키워드 추출의 성능을 측정하기 위한 데이터셋이 존재하지 않기 때문에 50개의 자체 평가 데이터셋을 구성하였습니다. -## 기사 긍부정 분류 +## 기사 감성 분석 -- 추출된 키워드가 기업의 좋은 상황을 나타내는 단어인지, 나쁜 상황을 나타내는 단어인지 정보를 제공하기 위해 긍부정 분류 모델을 사용합니다. 기사 전체를 감성 분석한 뒤에 키워드의 대용 지표로 채택합니다. -- 총 학습 데이터는 기사 긍부정 분류에는 30개의 기업의 총 9000개의 기사를 chat gpt API를 활용해 긍부정 labeling을 진행했고, train set, dev set을 8:2로 나누어서 학습을 진행했습니다. +- 추출된 키워드가 기업의 좋은 상황을 나타내는 단어인지, 나쁜 상황을 나타내는 단어인지 정보를 제공하기 위해 감성분석 모델을 사용합니다. 기사 전체를 감성 분석한 뒤에 키워드의 대용 지표로 채택합니다. +- 학습 데이터로 30개의 기업 기업의 기사 4개 기업(3800개)+26개 기업(520개)의 기사를 chat gpt API를 활용해 긍부정 labeling을 진행했고, train set, dev set을 8:2로 나누어서 학습을 진행했습니다. ## 기사 요약 -- IT / 경제분야 뉴스를 이용하여 학습한 모델(T5, polyglot-ko)을 이용하여 뉴스를 요약 제공합니다. -- T5 모델을 이용하여 한줄 요약을 만들어내고, 이후 자세한 내용은 polyglot-ko 모델을 이용하여 상대적으로 긴 요약 내용을 추가해 줍니다. +- IT / 경제분야 뉴스를 이용하여 학습한 모델(`T5`, `polyglot-ko`)을 이용하여 뉴스를 요약 제공합니다. +- `T5` 모델을 이용하여 한줄 요약을 만들어내고, 이후 자세한 내용은 `polyglot-ko` 모델을 이용하여 상대적으로 긴 요약 내용을 추가해 줍니다. - 모델을 이용하여 생성된 결과는 유의미한 문장만을 가져와 후처리하여 반환합니다. ---- +# 🏗️ 프로젝트 구조 +![](/assets/img/proj_structure.png) -# 👨‍🔬 모델 연구 +# 👨‍🔬 사용 모델 ## 키워드 추출 +- `TF-IDF` Vectorizer로 문서를 벡터로 표현하여, 상위 가중치를 갖는 단어를 주요 단어 후보로 선정했습니다. - 주요 단어 후보와 기사를 한국어로 기학습된 `Sentence-Transformer`를 이용해서 Embedding을 계산한 후, 유사도를 계산하여 높은 점수를 낸 단어를 해당 기사의 주요 키워드로 선정하였습니다. - 선정된 키워드들을 키워드의 형태(명사형 어구)로 표시하고자, 추출된 키워드에 대해 후처리를 진행하였습니다. ## 뉴스 긍부정 분류 -## 기사 요약 \ No newline at end of file +- 최대 1900 토큰의 길이에 달하는 기사들을 첫 128 토큰과 마지막 384토큰만을 사용하여, 총 512토큰으로 요약하여 모델에 입력하였습니다. +- 모델은 `klue/RoBERTa-large`를 사용하였습니다. + +## 기사 요약 + +- 논문 요약 데이터셋과 IT / 경제분야 뉴스 요약 데이터셋을 실험한 결과 선정된 IT / 경제분야 뉴스 요약 데이터셋을 선정하여 요약 모델을 학습시켰습니다. +- 선정된 데이터셋을 이용하여 본 서비스에서 사용될 `T5-base`, `polyglot-ko 1.3b` 모델을 학습시켰습니다. + +# 데모 영상 + +![](/assets/img/demo.gif) + +# 🔗 링크 + +- [랩업 리포트](/assets/docs/NLP_04_Wrap-Up_Report_FinalProj.pdf) +- [프로젝트 소개 노션 페이지](https://www.notion.so/0375bff1dc834ead9f6a8f9ae8baa90e?pvs=21) +- [최종 발표 영상](https://youtu.be/KgvPInfO6k8) +- [서비스 바로가기](https://deagul.netlify.app/) diff --git a/Sentimental/dataset/datasets.py b/Sentimental/dataset/datasets.py index 00c401e..ae7cd5f 100644 --- a/Sentimental/dataset/datasets.py +++ b/Sentimental/dataset/datasets.py @@ -4,12 +4,15 @@ class SentimentalDataset(Dataset): def __init__(self, encodings, labels): self.encoding = encodings - self.labels = labels[self.encoding['overflow_to_sample_mapping'].tolist()].reset_index(drop=True) - # 여러 개로 나뉜 문장에 대해 1개의 라벨을 매핑해줍니다. + try: + self.labels = labels[self.encoding['overflow_to_sample_mapping'].tolist()].reset_index(drop=True) + # return_overflowing_tokens true의 경우 여러 개로 나뉜 문장에 대해 1개의 라벨을 매핑해줍니다. + except: + self.labels = labels def __getitem__(self, idx): data = {key: val[idx] for key, val in self.encoding.items()} - data['labels'] = torch.tensor(self.labels[idx]).long() + data['label'] = torch.tensor(self.labels[idx]).long() return data def __len__(self): diff --git a/Sentimental/evaluate.py b/Sentimental/evaluate.py new file mode 100644 index 0000000..05af9aa --- /dev/null +++ b/Sentimental/evaluate.py @@ -0,0 +1,102 @@ +import torch +import pandas as pd +import sklearn +import random +import numpy as np +import wandb + +from transformers import AutoModel +from transformers import AutoModelForSequenceClassification, AutoTokenizer +from transformers import TrainingArguments, Trainer +from transformers import pipeline +from transformers import DebertaV2ForSequenceClassification + +from dataset.datasets import SentimentalDataset +from metrics.metrics import compute_metrics + +from sklearn.datasets import load_iris # 샘플 데이터 로딩 +from sklearn.model_selection import train_test_split + +from utils.utils import config_seed, gpt_preprocessing_labels, extract_sentences_token, gpt_preprocessing_labels_token + +import json +import re + + +# 설정 +SEED = 42 +config_seed(SEED) +device = torch.device("cuda" if torch.cuda.is_available() else 'cpu') + + +# 모델 +name = 'name' +MODEL_NAME = "/opt/ml/level3_nlp_finalproject-nlp-04/Sentimental/outputs/klue/roberta-large_merged-4_100-26_50" +model = AutoModelForSequenceClassification.from_pretrained(MODEL_NAME, num_labels=2).to(device) +tokenizer = AutoTokenizer.from_pretrained("klue/roberta-large") + +special_tokens_dict = {'additional_special_tokens': ['[COMPANY]','[/COMPANY]']} +num_added_toks = tokenizer.add_special_tokens(special_tokens_dict) +model.resize_token_embeddings(len(tokenizer)) + + +# 데이터 +data = pd.read_csv("/opt/ml/finance_sentiment_corpus/merged/merged_all.csv") +data = gpt_preprocessing_labels_token(data) # gpt에서 출력한 오류들을 json 형식으로 맞춰주고 labels를 수정하는 것 + +dataset = train_test_split(data['content_corpus_company'], data['labels'], + test_size=0.2, shuffle=True, stratify=data['labels'], + random_state=SEED) + + +sentence_train, sentence_val, label_train, label_val = dataset + + +max_length=3000 +stride=0 + +val_encoding = tokenizer(sentence_val.tolist(), + return_tensors='pt', + padding=True, + truncation=True, + ## + max_length=max_length, + # stride=stride, + # return_overflowing_tokens=True, + # return_offsets_mapping=False + ) + +val_encoding = extract_sentences_token(val_encoding, tokenizer.pad_token_id) +# 앞 128, 뒷 384 토큰으로 센텐스를 추출합니다. + +val_set = SentimentalDataset(val_encoding, label_val.reset_index(drop=True)) + + +# evaluate +logging_steps = 200 +num_train_epochs = 1 +per_device_train_batch_size = 8 +per_device_eval_batch_size = 8 +learning_rate = 5e-6 + +training_args = TrainingArguments( + output_dir = './outputs', + logging_steps = logging_steps, + num_train_epochs = num_train_epochs, + per_device_train_batch_size = per_device_train_batch_size, + per_device_eval_batch_size = per_device_eval_batch_size, + learning_rate = learning_rate, + evaluation_strategy="epoch", + fp16=True, + report_to="wandb", +) + +trainer = Trainer( + model=model, + args=training_args, + # train_dataset=train_set, + eval_dataset=val_set, + compute_metrics=compute_metrics +) + +trainer.evaluate(eval_dataset=val_set) \ No newline at end of file diff --git a/Sentimental/inference_from_saving.py b/Sentimental/inference_from_saving.py new file mode 100644 index 0000000..cd45a41 --- /dev/null +++ b/Sentimental/inference_from_saving.py @@ -0,0 +1,43 @@ +import torch +from transformers import AutoTokenizer, AutoModelForSequenceClassification +from utils.utils import extract_sentences_token + + +def inference() : + MODEL_PATH = "" #TODO: write_your_model_path + tokenizer = AutoTokenizer.from_pretrained("klue/roberta-large") + model = AutoModelForSequenceClassification.from_pretrained(MODEL_PATH) + device = 'cuda' if torch.cuda.is_available() else 'cpu' + model = model.to(device) + + arr = ["이 기사는[COMPANY] 예시 기업 1 [/COMPANY]에 대한 기사야. [SEP] news corpus 1", + "이 기사는[COMPANY] 예시 기업 2 [/COMPANY]에 대한 기사야. [SEP] news corpus 2"] + answer = [] + + model.eval() + with torch.no_grad() : + loader = torch.utils.data.DataLoader(dataset=arr, batch_size=32, shuffle=False) + for batch_text in loader: + temp = tokenizer( + batch_text, + return_tensors='pt', + padding='max_length', + truncation=True, + max_length=3000 + ) + temp = extract_sentences_token(temp, tokenizer.pad_token_id) + if torch.cuda.is_available(): + temp = {key: value.to(device) for key, value in temp.items()} + predicted_label = model(**temp) + print(predicted_label) + + results = torch.nn.Softmax(dim=-1)(predicted_label.logits) + for result in results : + if result[0]>=result[1] : + answer.append("부정") + else : + answer.append("긍정") + print(answer) + +if __name__== "__main__" : + inference() \ No newline at end of file diff --git a/Sentimental/main.py b/Sentimental/main.py index af3eba2..72811d5 100644 --- a/Sentimental/main.py +++ b/Sentimental/main.py @@ -1,4 +1,6 @@ from train import sweep_train +from inference_from_saving import inference + import wandb import argparse @@ -23,5 +25,4 @@ wandb.agent(sweep_id, function=sweep_train) elif args.inference : - # TODO: inference 코드 구현 - pass \ No newline at end of file + inference() \ No newline at end of file diff --git a/Sentimental/train.py b/Sentimental/train.py index fcd7175..da5930f 100644 --- a/Sentimental/train.py +++ b/Sentimental/train.py @@ -17,14 +17,13 @@ from sklearn.datasets import load_iris # 샘플 데이터 로딩 from sklearn.model_selection import train_test_split -from utils.utils import config_seed, gpt_preprocessing_labels +from utils.utils import config_seed, extract_sentences_token, gpt_preprocessing_labels_token -import json -import re ''' - git clone https://github.com/ukairia777/finance_sentiment_corpus.git 먼저 실행 - readme.md 작성 예정 ''' + def sweep_train(config=None) : # wandb run = wandb.init(config=wandb.config) @@ -48,16 +47,23 @@ def sweep_train(config=None) : # 데이터 data = pd.read_csv("/opt/ml/finance_sentiment_corpus/merged/merged_all.csv") - data = gpt_preprocessing_labels(data) # gpt에서 출력한 오류들을 json 형식으로 맞춰주고 labels를 수정하는 것 + data_val = pd.read_csv("/opt/ml/finance_sentiment_corpus/26_company_half_rest_labeled.csv") + + data = gpt_preprocessing_labels_token(data) # gpt에서 출력한 오류들을 json 형식으로 맞춰주고 labels를 수정하는 것 + data_val = gpt_preprocessing_labels_token(data_val) - # "labels" 값을 추출하여 새로운 Series 생성 - dataset = train_test_split(data['content_corpus_company'], data['labels'], - test_size=0.2, shuffle=True, stratify=data['labels'], # label에 비율을 맞춰서 분리 + # "labels" 값을 추출하여 train/val dataset 나누기 + dataset = train_test_split(data['content_corpus_company'], data['label'], + test_size=0.2, shuffle=True, stratify=data['label'], # label에 비율을 맞춰서 분리 random_state=SEED) sentence_train, sentence_val, label_train, label_val = dataset + sentence_train = data["content_corpus"] + label_train = data["label"] + sentence_val = data_val["content_corpus"] + label_val = data_val["label"] max_length=wandb.config.max_length #원본:500 stride=0 @@ -67,23 +73,24 @@ def sweep_train(config=None) : return_tensors='pt', padding=True, truncation=True, - ## max_length=max_length, - stride=stride, - return_overflowing_tokens=True, - return_offsets_mapping=False + # stride=stride, + # return_overflowing_tokens=True, + # return_offsets_mapping=False ) val_encoding = tokenizer(sentence_val.tolist(), return_tensors='pt', padding=True, truncation=True, - ## max_length=max_length, - stride=stride, - return_overflowing_tokens=True, - return_offsets_mapping=False + # stride=stride, + # return_overflowing_tokens=True, + # return_offsets_mapping=False ) + + train_encoding = extract_sentences_token(train_encoding, tokenizer.pad_token_id) + val_encoding = extract_sentences_token(val_encoding, tokenizer.pad_token_id) # 앞 128, 뒷 384 토큰으로 센텐스를 추출합니다. train_set = SentimentalDataset(train_encoding, label_train.reset_index(drop=True)) val_set = SentimentalDataset(val_encoding, label_val.reset_index(drop=True)) @@ -92,8 +99,8 @@ def sweep_train(config=None) : # 학습 logging_steps = 200 num_train_epochs = 3 - per_device_train_batch_size = 4 - per_device_eval_batch_size = 4 + per_device_train_batch_size = 8 + per_device_eval_batch_size = 8 learning_rate = 5e-6 training_args = TrainingArguments( @@ -120,13 +127,15 @@ def sweep_train(config=None) : trainer.train() # wandb.finish() + name = f'{MODEL_NAME}_merged-4_100-26_50' # 원하는 이름으로 바꾸기 + trainer.save_model(output_dir=f'./outputs/{name}') if __name__ == '__main__': wandb_config = { 'method': 'grid', 'parameters': { - 'max_length': {'values' : [500, 250]}, + 'max_length': {'values' : [3000]}, # 충분히 커서 본문 전체를 토크나이징. } } sweep_id = wandb.sweep(sweep=wandb_config, diff --git a/Sentimental/utils/utils.py b/Sentimental/utils/utils.py index 6cd25ea..304354f 100644 --- a/Sentimental/utils/utils.py +++ b/Sentimental/utils/utils.py @@ -16,12 +16,12 @@ def remove_idx_row(data) : patterns = [r'idx\s*:?\s*.+?', r'라벨링\s*:?\s*.+?'] for pattern in patterns : - mask = data['labels'].str.match(pattern) + mask = data["label"].str.match(pattern) data = data.drop(data[mask].index) return data -# ['labels'] 전처리 +# ['label'] 전처리 def preprocessing_label(json_str) : json_str = re.sub(r"^.*### 출력\s?:?\s?\n?\s?", "", str(json_str)) # json_str= json_str.replace("\"", "'") @@ -39,6 +39,35 @@ def extract_sentences(text): else : return '. '+text +# 512 토큰 초과인 input_ids, token_type_ids, attention_maks를 +# 앞 128, 뒤 384으로 분리합니다. +def extract_sentences_token(input_dict, pad_token_id): + ''' + 사용 방법: + train_encoding = extract_sentences_token(train_encoding, tokenizer.pad_token_id) + ''' + new = {} + batch_size = len(input_dict['input_ids']) + new['input_ids'] = torch.ones(batch_size, 512, dtype=int) + new['token_type_ids'] = torch.ones(batch_size, 512, dtype=int) + new['attention_mask'] = torch.ones(batch_size, 512, dtype=int) + # batch_size, 512 + for i in range(batch_size): + a = input_dict['input_ids'][i] + a = a[a != pad_token_id] + length = len(a) + if length > 512: + left, right = 1, 3 + a = torch.cat((a[:128*left], a[-128*right:]), dim=0) + new['input_ids'][i] = a + new['token_type_ids'][i] = input_dict['token_type_ids'][i][:512] + new['attention_mask'][i] = input_dict['attention_mask'][i][:512] + else: + new['input_ids'][i] = input_dict['input_ids'][i][:512] + new['token_type_ids'][i] = input_dict['token_type_ids'][i][:512] + new['attention_mask'][i] = input_dict['attention_mask'][i][:512] + return new + # json 형태로 되어있지 않은 형태를 최대한 json 형태로 바꿔주고 label 추출 def extract_label(json_str) : json_str = json_str.replace("'", "\"") @@ -62,12 +91,30 @@ def extract_label(json_str) : return data_dict["label"] def gpt_preprocessing_labels(data) : + if "label" not in data.columns : + data["label"] = data["labels"] + + data = remove_idx_row(data) + data["label"] = data["label"].apply(preprocessing_label) + data["label"] = data["label"].apply(extract_label) + data["content_corpus_company"] = data.apply(lambda row: '이 기사는 [COMPANY]'+ str(row["company"]) +'[/COMPANY]에 대한 기사. [SEP]'+ extract_sentences(row["title"]) + ' ' + extract_sentences(row["content_corpus"]), axis=1) #TODO: 학습에 들어가는 부분이기 때문에 조정 필수 + data["label"] = data["label"].map({"부정":0, "긍정":1}) + data = data[["title", "date", "content_corpus", "label", "content_corpus_company"]] + data = data[data["label"].notna()] # 최후에 처리 되지 않은 None row 제거 + + return data + + +def gpt_preprocessing_labels_token(data) : + if "label" not in data.columns : + data["label"] = data["labels"] + data = remove_idx_row(data) - data["labels"] = data["labels"].apply(preprocessing_label) - data['labels'] = data["labels"].apply(extract_label) - data['content_corpus_company'] = data.apply(lambda row: '이 기사는 [COMPANY]'+ str(row["company"]) +'[/COMPANY]에 대한 기사. [SEP]'+ extract_sentences(row['title']) + ' ' + extract_sentences(row['content_corpus']), axis=1) #TODO: 학습에 들어가는 부분이기 때문에 조정 필수 - data['labels'] = data['labels'].map({'부정':0, '긍정':1}) - data = data[["title", "date", "content_corpus", "labels", "content_corpus_company"]] - data = data[data['labels'].notna()] # 최후에 처리 되지 않은 None row 제거 + data["label"] = data["label"].apply(preprocessing_label) + data["label"] = data["label"].apply(extract_label) + data["content_corpus_company"] = data.apply(lambda row: '이 기사는 [COMPANY]'+ str(row["company"]) +'[/COMPANY]에 대한 기사. [SEP]'+ (row['content_corpus']), axis=1) #TODO: 학습에 들어가는 부분이기 때문에 조정 필수 + data["label"] = data["label"].map({'부정':0, '긍정':1}) + data = data[["title", "date", "content_corpus", "label", "content_corpus_company"]] + data = data[data["label"].notna()] # 최후에 처리 되지 않은 None row 제거 return data \ No newline at end of file diff --git a/airflow/dags/crawl_naver_finance_news.py b/airflow/dags/crawl_naver_finance_news.py new file mode 100644 index 0000000..182040d --- /dev/null +++ b/airflow/dags/crawl_naver_finance_news.py @@ -0,0 +1,44 @@ +import sys +import pendulum + +from airflow import DAG +from airflow.operators.python import PythonOperator +from datetime import datetime, timedelta + +sys.path.append('/opt/ml/level3_nlp_finalproject-nlp-04/utils') +from crawling_utils import crawl_finance_news + +tz = pendulum.timezone("Asia/Seoul") +default_args = { + 'owner': 'yungi', + 'depends_on_past' : False, + 'start_date' : datetime(2023, 7, 18, 15), # UTC 시간 -> KST +9 시간 하면 7월 19일부터 + 'retires' : 1, + 'retry_delay' : timedelta(minutes=5), + } +def print_date(timestr): + print("***** Execution Time *****:",timestr) + +with DAG( + dag_id="crawl_naver_finance_news_v0.2.1", + user_defined_macros={'local_dt' : lambda execution_date: execution_date.in_timezone(tz).strftime("%Y-%m-%d")}, # KST시간으로 Timezone 변경(+9시간) 매크로 + default_args=default_args, + schedule_interval="0 15,21,3,9 * * *", # 크롤링 어느정도 끝나고 현재로 오면, 6시간마다 크롤링하도록(UTC시간 기준시작, 15부터 해야지 KST=다음날 0시) + # schedule_interval="0 0 * * *", # 과거일자 크롤링할 땐 그냥 하루단위로 작업하도록 + tags = ['crawl'] +) as dag: + execution_date = "{{ local_dt(execution_date) }}" # 실제 실행 시간은 KST기준(+9시간)으로 수행하도록 + # execution_date = "{{ ds }}" + python_task_jinja = PythonOperator( + task_id="crawl_naver_finance_news", + python_callable=crawl_finance_news, + op_kwargs={'cur_time' : execution_date} + ) + print_date_jinja = PythonOperator( + task_id="print_date", + python_callable=print_date, + op_args=[execution_date] + ) + + print_date_jinja >> python_task_jinja + diff --git a/airflow/dags/model_serving.py b/airflow/dags/model_serving.py new file mode 100644 index 0000000..bcc6a10 --- /dev/null +++ b/airflow/dags/model_serving.py @@ -0,0 +1,186 @@ +import sys +import pendulum +import json +import requests + +from airflow import DAG +from airflow.operators.python import PythonOperator +from datetime import datetime, timedelta +from pathlib import Path +from supabase import Client, create_client +from collections import Counter, defaultdict + + +sys.path.append('/opt/ml/level3_nlp_finalproject-nlp-04') +from utils.secrets import Secrets +from utils.preprocessing import * + +tz = pendulum.timezone("Asia/Seoul") +default_args = { + 'owner': 'yungi', + 'depends_on_past' : False, + 'start_date' : datetime(2023, 7, 30, 15), # UTC 시간 -> KST +9 시간 하면 7월 19일부터 + 'retires' : 1, + 'retry_delay' : timedelta(minutes=5), + } +url = Secrets.url +key = Secrets.key + +supabase: Client = create_client(url, key) + + +def get_news(cur_time,content_length_thr): + print("**** Current Time **** :",cur_time) + cur_time = datetime.strptime(cur_time, "%Y-%m-%d %H:%M:%S") + stock_name = supabase.table("ticker").select("name").eq("ticker", "005930").execute().data[0]["name"] + res = supabase.table("news").select("*").eq("company", stock_name).filter("date", "gt",(cur_time - timedelta(hours=7)).strftime('%Y-%m-%d %H:%M:%S') + ).filter("date", "lt",cur_time.strftime('%Y-%m-%d %H:%M:%S')).execute().data + # 짧은 뉴스 제거 + for cont in res: + if len(cont['content']) <= content_length_thr: + res.remove(cont) + return res + +def get_sentimental(text): + articles = eval(text) + res_sentimental = requests.post("http://115.85.183.242:30007/classify_sentiment", + data=json.dumps({ + "corpus_list" : [all_preprocessing(news['content']) for news in articles], + "company_list" : [news['company'] for news in articles], + })).content + return json.loads(res_sentimental) + +def get_keywords(text): + articles = eval(text) + keywords = json.loads(requests.post("http://118.67.133.198:30008/keywordExtraction/5", + data=json.dumps({ + "data_input" : { + "titles" : articles['title'] if type(articles) == dict else [all_preprocessing(news['title']) for news in articles ], + "contents" : articles['content'] if type(articles) == dict else [all_preprocessing(news['content']) for news in articles], + "dates" : articles['date'] if type(articles) == dict else [news['date'] for news in articles], + }, + "stop_words" : ["삼성", "전자", "삼성전자"]}), + params={"vectorizer_type" : "tfidf"} + ).content) + return keywords + +def get_summary(articles, **config): + pp = requests.post(f"http://118.67.143.119:30007/api/v1/summerize/{config['model_name']}", + data=json.dumps({ + "prompt": "아래의 뉴스를 사실에 입각하여 맥락의 내용을 요약해줘.", + "title" : articles['title'], + "contents" : all_preprocessing(articles['content']), + "num_split" : config["num_split"], + "options": { + "do_sample": True, + "early_stopping": "never", + "eos_token_id": 2, + "max_new_tokens": config["max_new_tokens"], + "no_repeat_ngram_size": config["no_repeat_ngram_size"], + "temperature" : config["temperature"], + "top_k": config["top_k"], + "top_p": config["top_p"], + } + } + ) + ).content + return json.loads(pp)['arr_summerized'] + +def filter_push_keyword_sentimental(news, keywords, sentimentals, cur_time, score_thr=0.2): + pos_cnt = Counter() + neg_cnt = Counter() + pos_keyword_id = defaultdict(list) + neg_keyword_id = defaultdict(list) + + news = eval(news) + keywords = eval(keywords) + sentimentals = eval(sentimentals) + + for i, sent in enumerate(sentimentals): + if sent == "긍정": + for keyword, score in keywords[0][i]: + if score > score_thr: + pos_cnt[keyword] += 1 + pos_keyword_id[keyword].append(news[i]['id']) + elif sent == "부정": + for keyword, score in keywords[0][i]: + if score > score_thr: + neg_cnt[keyword] += 1 + neg_keyword_id[keyword].append(news[i]['id']) + + for key in set(list(pos_cnt.keys()) + list(neg_cnt.keys())): + pos_c = pos_cnt.get(key, 0) + neg_c = neg_cnt.get(key, 0) + pos_k = pos_keyword_id.get(key, []) + neg_k = neg_keyword_id.get(key, []) + supabase.table("keywords").insert({"keyword": key, + "count": pos_c + neg_c, + "create_time": datetime.strptime(cur_time, "%Y-%m-%d %H:%M:%S").strftime( + "%Y-%m-%d %H:%M:%S"), + "pos_cnt": pos_c, + "neg_cnt": neg_c, + "summary_id": { + "pos_news": pos_k, + "neg_news": neg_k + } + }).execute() + return {"pos_cnt" : pos_cnt, "neg_cnt" : neg_cnt, "pos_keyword_id" : pos_keyword_id, "neg_keyword_id" : neg_keyword_id} + +def push_summary(text): + config = { + "model_name" : "t5", + "num_split" : 9999, + "max_new_tokens" : 256, + "no_repeat_ngram_size" : 8, + "temperature" : 0.08, + "top_k" : 50, + "top_p" : 0.98, + } + articles = eval(text) + for i, news in enumerate(articles): + summary = get_summary(news, **config)[0] + supabase.table("news_summary").insert({"origin_id": articles[i]['id'], "summarization": summary}).execute() + +with DAG( + dag_id="model_serving_v0.1.0", + user_defined_macros={'local_dt' : lambda execution_date: execution_date.in_timezone(tz).strftime("%Y-%m-%d %H:%M:%S")}, # KST시간으로 Timezone 변경(+9시간) 매크로 + default_args=default_args, + schedule_interval="0 16,22,4,10 * * *", # 크롤링 어느정도 끝나고 현재로 오면, 6시간마다 크롤링하도록(UTC: 15 = KST:다음날 0시) + # schedule_interval="0 0 * * *", # 과거일자 크롤링할 땐 그냥 하루단위로 작업하도록 + catchup=True, + tags = ['serving'] +) as dag: + execution_date = "{{ local_dt(execution_date) }}" # 실제 실행 시간은 KST기준(+9시간)으로 수행하도록 + # execution_date = "{{ ds }}" + + get_news = PythonOperator( + task_id="get_news", + python_callable=get_news, + op_kwargs={'cur_time': execution_date, + "content_length_thr" : 400} + ) + get_keywords = PythonOperator( + task_id="get_keywords", + python_callable=get_keywords, + op_args=["{{task_instance.xcom_pull(task_ids='get_news')}}"] + ) + get_sentimental = PythonOperator( + task_id="get_sentimental", + python_callable=get_sentimental, + op_args=["{{task_instance.xcom_pull(task_ids='get_news')}}"] + ) + get_push_summary = PythonOperator( + task_id="get_push_summary", + python_callable=push_summary, + op_args=["{{task_instance.xcom_pull(task_ids='get_news')}}"], + ) + filter_push_keyword_sentimental = PythonOperator( + task_id="filter_keyword_sentimental", + python_callable=filter_push_keyword_sentimental, + op_kwargs={"news": "{{task_instance.xcom_pull(task_ids='get_news')}}", + "keywords": "{{task_instance.xcom_pull(task_ids='get_keywords')}}", + "sentimentals": "{{task_instance.xcom_pull(task_ids='get_sentimental')}}", + "cur_time" : execution_date, + "score_thr" : 0.2} + ) + get_news >> [get_keywords, get_sentimental, get_push_summary] >> filter_push_keyword_sentimental \ No newline at end of file diff --git a/assets/docs/NLP_04_Wrap-Up_Report_FinalProj.pdf b/assets/docs/NLP_04_Wrap-Up_Report_FinalProj.pdf new file mode 100644 index 0000000..e4819d2 Binary files /dev/null and b/assets/docs/NLP_04_Wrap-Up_Report_FinalProj.pdf differ diff --git a/assets/img/demo.gif b/assets/img/demo.gif new file mode 100644 index 0000000..ec56763 Binary files /dev/null and b/assets/img/demo.gif differ diff --git a/assets/img/proj_structure.png b/assets/img/proj_structure.png new file mode 100644 index 0000000..89e306f Binary files /dev/null and b/assets/img/proj_structure.png differ diff --git a/labeling/gpt_api.py b/labeling/gpt_api.py index a232026..4ea7bb1 100644 --- a/labeling/gpt_api.py +++ b/labeling/gpt_api.py @@ -16,25 +16,27 @@ def get_completion(prompt, model="gpt-3.5-turbo"): return response.choices[0].message["content"] -def get_label(news: str) -> str: +def get_label(company, news: str) -> str: key = os.environ['OPENAPI_KEY'] openai.api_key = key prompt = f'''### 역할: - 너는 어느 증권사 재무팀에서 일하고 있는 애널리스트야. 너의 주 업무는 뉴스 기사를 보고 해당 기사가 "긍정" 혹은 "부정"인지 라벨링 하는 것이야. 이때 아래의 규칙을 따라야해. - + 너는 어느 증권사 재무팀에서 일하고 있는 애널리스트야. 너의 주 업무는 뉴스 기사를 보고 {company} 입장에서 해당 기사가 "긍정" 혹은 "부정"인지 라벨링 하는 것이야. 이때 아래의 규칙을 따라야해. + + ### 기사: + {news} + ### 규칙: 1. 주어진 기사에 대해 하나의 ["긍정", "부정"]만을 선택하여 라벨링한다. 2. 기사는 "### 기사:" 부분부터 시작된다. 3. 출력은 "### 출력"와 같이 json 형식으로 출력한다. 4. 라벨링은 기사 전문에 대해서 라벨링한다. 5. 중립은 선택지에 없다. + 6. 출력은 형식외에 아무것도 작성하지 않는다. ### 출력 {{"label": "긍정", "reason": "새로운 투자 유치로 인해 해당 기업의 전망이 밝을것으로 예측된다. "}} - - ## 기사: - {news}''' + ''' response = get_completion(prompt) return response @@ -53,7 +55,7 @@ def get_label(news: str) -> str: for idx, row in tqdm(df_news.iterrows(), total=df_news.shape[0]): try: - list_result.append(get_label(row["content_corpus"])) + list_result.append(get_label(row["company"], row["content_corpus"])) except Exception as e: print(f"idx: {idx}, err: {e}") list_result.append(f"idx: {idx}, err: {e}") diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/crawling_utils.py b/utils/crawling_utils.py new file mode 100644 index 0000000..6179ce2 --- /dev/null +++ b/utils/crawling_utils.py @@ -0,0 +1,152 @@ +import requests +import re +import pandas as pd +import time +import sys + +from pathlib import Path +from selectolax.parser import HTMLParser, Node +from supabase import create_client, Client +from tqdm import tqdm, trange +from datetime import datetime +from typing import List, Union + +sys.path.append(str(Path.home().joinpath("level3_nlp_finalproject-nlp-04"))) +from utils.secrets import Secrets + +SECRETS = Secrets() + +def crawl_finance_news(cur_time: str, + stock_nums: int = 30, + delay: float = 0.0, + threshold_num: int = 10,) -> pd.DataFrame: + """ + 네이버 금융 뉴스를 크롤링하는 함수입니다. + + Params: + cur_time (str): 현재 시간 + stock_nums (int): KRX300 기준 크롤링할 종목의 개수 + delay (float): 요청 지연 시간 + threshold_num (int): 일자가 넘어간 기사 용인 갯수 + + Returns: + pandas.DataFrame: 크롤링한 뉴스 데이터프레임 + """ + assert stock_nums <= 300, "stock_nums를 300개 이내로 입력해주세요." + + BASE_DIR = Path.home().joinpath("level3_nlp_finalproject-nlp-04") + NEWS_DIR = BASE_DIR.joinpath("Data", "News") + STOCK_DIR = BASE_DIR.joinpath("Data", "Stock") + + # 현재 날짜 + cur_time = datetime.strptime(cur_time, "%Y-%m-%d") + + stock_detail = pd.read_csv(STOCK_DIR.joinpath("stock_krx300.csv"), encoding="cp949") + stock_detail = stock_detail.loc[:, ["종목코드", "종목명"]] + stock_detail["종목코드"] = stock_detail["종목코드"].apply(lambda x: str(x).zfill(6)) + + pbar = tqdm(zip(stock_detail["종목코드"][:stock_nums], stock_detail["종목명"][:stock_nums]), desc="종목별 뉴스 크롤링") + for stock_code, company in pbar: + pbar.set_description(f"종목: {stock_code}|{company}") + threshold_cnt = 0 + # 뉴스 페이지의 끝을 찾기 위해 크롤링 수행 + page_max_url = f"https://finance.naver.com/item/news_news.naver?code={stock_code}" + page_max_html = requests.get(page_max_url).text + page_max_tree = HTMLParser(page_max_html) + nodes = page_max_tree.css("body > div > table.Nnavi > tbody > tr > td.pgRR > a") + + try: + PAGE_MAX = int(re.findall(r"(?<=page=)\d+", nodes[-1].attributes['href'])[0]) + except IndexError: + print("기사가 존재하지 않습니다.") + return + content_df = pd.DataFrame([], columns=['company', 'title', 'link', "writer", 'date', 'content']) if NEWS_DIR.joinpath(stock_code + ".csv").exists() is False else pd.read_csv(NEWS_DIR.joinpath(stock_code + ".csv"), encoding="utf-8") + + + # 전체 페이지 순환 + for i in trange(1, PAGE_MAX + 1): + url = f"https://finance.naver.com/item/news_news.naver?code={stock_code}&page={i}&sm=title_entity_id.basic&clusterId=" + html = requests.get(url).text + + tree = HTMLParser(html) + nodes = tree.css("body > div.tb_cont > table.type5 > tbody > tr") + + # 연관기사로 묶여있는 것들 제거 + for node in nodes: + if len(node.attributes) != 0 and "relation_lst" in node.attributes["class"]: + node.decompose() + + # 해당 페이지의 기사들을 content_df에 추가 + for node in nodes: + content = [] + tds = node.css("td") + + # company -> title -> link -> writer -> date 순으로 추출 + content.append(company) + # 연관기사로 묶여있는 리스트가 비어있기 때문에, 아닌 것들만 기사 추출 + if len(tds) != 0: + for td in tds: + content.append(td.text(strip=True)) + if len(td.css("a")) != 0: + content.append(td.css("a")[0].attributes["href"]) + + title = content[1] + link = "https://finance.naver.com" + re.findall(r"\S+(?=\&page=)", content[2])[0] # link의 page앞으로만이 실제 유효한 기사 링크 + content[2] = link + writer = content[3] + date = content[4] + + written_date = datetime.strptime(date, '%Y.%m.%d %H:%M') + if (written_date.date() == cur_time.date()) and not (content_df['link'] == link).any(): # 날짜가 같은 날에 작성된 기사만 크롤링 + # 본문 추출을 위해 위에서 link를 이용해 html을 다시 요청 + content_html = requests.get(link).text + content_tree = HTMLParser(content_html) + + # 본문이 없는 기사(삭제된 기사)가 종종 존재 -> 예외처리 + try: + res = content_tree.css( + "body > div#wrap > div#middle.new_totalinfo > div.content_wrap > div#content > div.section.inner_sub > table.view > tbody > tr > td > div")[0].html + except IndexError: + continue + + # 본문 추출 + # 기사본문에 해당하는 부분만 뽑기 위해