Data Science Project

건설공사 사고 예방 및 대응책 생성 : 한솔데코 시즌3 AI 경진대회

ParkS2 2025. 3. 21. 23:05

이번 프로젝트는 한솔데크에서 주관하는 건설공사 데이터를 활용한 대응책 생성 AI 개발이다.

 

데이터 정보는 다음과 같다.

 


Dataset Info.

train.csv [파일] - 학습 가능

ID : 샘플별 고유 ID

발생일시

사고인지 시간

날씨

기온

습도

공사종류

연면적

층 정보

인적사고

물적사고

공종

사고객체

작업프로세스

장소

부위

사고원인

재발방지대책 및 향후조치계획

 

건설안전지침 [폴더] - 학습 가능

104개의 건설안전지침 PDF

 

test.csv [파일]

ID : 샘플별 고유 ID

발생일시

사고인지 시간

날씨

기온

습도

공사종류

연면적

층 정보

인적사고

물적사고

공종

사고객체

작업프로세스

장소

부위

사고원인


 

train데이터를 활용해서 각 인적사고 칼럼의 데이터 값을 기준으로 대응책 텍스트를 그룹화 하였으며, 코사인 유사도를 사용하여 가장 각 그룹마다 가장 유사도가 높은 문장(가장 보편적인 문장)을 선별하는식으로 진행을 하였다.

 

참고로, 건설안전지침 [폴더] 데이터가 있었지만, 다양한 텍스트의 종류와 현재 로컬에서 돌릴수있는 모델에서 정확한 텍스트의 전처리 및 분류 가 어렵다고 판단하여 모델링 데이터로 활용하지는 않았다.

 

총 2가지로 나눠서 프로젝트를 진행하였다. 

 

첫번째 방법은 No RAG 방식이다. 

LLM을 거치지 않고 LLM의 정제 없이 유사도 측정하여 문장 선별 후에 바로 테스트 데이터에 적용하는것이다.

 

두번째 방법은 RAG 방식이다.

LLM을 통하여 RAG방식으로 유사도 계산을 통해 선별된 문장을 LLM으로 한번 정제를 하고 그 이후 테스트 데이터에 적용하는 방식이다.

 

먼저, 첫번째로 진행한 프로세스는 자연어 전처리이다.

임베딩을 진행하기전, 불용어 제거 및 명사만 남겨 문장의 의미파악을 용이하게 하기위해 진행을 하였다.

from konlpy.tag import Komoran
import re
import string

komoran = Komoran()

with open('C:/Users/82106/Desktop/데이콘 한솔데크/불용어.txt', 'r', encoding = 'UTF-8') as f:
  list_file = f.readlines() 
stopwords = list_file[0].split(",")

# 정규화
def preprocess(text):
    text=text.strip()  
    text=re.compile('<.*?>').sub('', text) 
    text = re.compile('[%s]' % re.escape(string.punctuation)).sub(' ', text)  
    text = re.sub('\s+', ' ', text)  
    text = re.sub(r'\[[0-9]*\]',' ',text) 
    text=re.sub(r'[^\w\s]', ' ', str(text).strip())
    text = re.sub(r'\d',' ',text) 
    text = re.sub(r'\s+',' ',text) 
    return text


# 명사/영단어 추출, 한글자 제외, 불용어 제거
def remove_stopwords(text):
    n = []
    word = komoran.nouns(text)
    p = komoran.pos(text)
    for pos in p:
      if pos[1] in ['SL']:
        word.append(pos[0])
    for w in word:
      if len(w)>1 and w not in stopwords:
        n.append(w)
    return " ".join(n)

# 최종
def finalpreprocess(text):
  return remove_stopwords(preprocess(text))

# 전처리 및 불용어 제거 적용 (두 단계)
train["재발방지대책 및 향후조치계획"] = train["재발방지대책 및 향후조치계획"].apply(finalpreprocess)

# 결과 확인
print(train["재발방지대책 및 향후조치계획"].head())

 

그다음으로 진행한 임베딩 이다. 

 

임베딩이란?

임베딩은 원본 데이터의 의미적 특성을 보존하면서 컴퓨터가 효율적으로 처리할 수 있는 수치 벡터로 표현하는 기술이다.

특히 자연어 처리에서는 단어나 문장을 밀집된 실수 벡터(dense vector)로 변환한다.

 

임베딩 과정

 

데이터 내의 "인적사고" 칼럼 값들을 group 메소드를 통해 그룹화하여 대응책 텍스트를 각 그룹으로 묶는다.

 

그 다음 코사인 유사도를 계산하기 위해 Sentence Transformer 의 ko-sbert-sts 을 통해 임베딩을 진행하였다.

 

from sentence_transformers import SentenceTransformer

# Embedding Vector 추출에 활용할 모델(jhgan/ko-sbert-sts) 불러오기
model = SentenceTransformer('jhgan/ko-sbert-sts', use_auth_token=False)

grouped = train.groupby("인적사고")

res = {}
cosine_res = []
for name, group in tqdm(grouped):
    plan = group["재발방지대책 및 향후조치계획"]
    vectors = np.stack(plan.apply(model.encode).to_numpy())
    similarity = cosine_similarity(vectors, vectors)    
    cosine_res += similarity[similarity.mean(axis=1).argmax()].tolist()
    res[name] = plan.iloc[similarity.mean(axis=1).argmax()]

 

그 다음 얼마나 보편적인 문장들(높은 유사도)이 선별되었는지 파악하기 위해 히스토그램으로 묶은후 시각화를 진행하였다.

 

arr = cosine_res

# 0.1 단위로 구간을 지정
bins = np.arange(0, 1.1, 0.1)  # 0.0 ~ 1.0을 0.1 간격으로 나눔

# 히스토그램 계산
hist, bin_edges = np.histogram(arr, bins=bins)

# 결과 출력
for i in range(len(hist)):
    print(f"Range {bin_edges[i]:.1f} - {bin_edges[i+1]:.1f}: {hist[i]}개")
    
import matplotlib.pyplot as plt

# 코사인 유사도 분포 시각화 (선 그래프)
arr = np.array(cosine_res)
bins = np.arange(0, 1.1, 0.1)
hist, bin_edges = np.histogram(arr, bins=bins)

# bin 간격의 중앙값 계산
bin_centers = 0.5 * (bin_edges[:-1] + bin_edges[1:])

plt.figure()
plt.plot(bin_centers, hist, marker='o', linestyle='-')
plt.title("Line Graph of Cosine Similarities Distribution")
plt.xlabel("Cosine Similarity")
plt.ylabel("Frequency")
plt.grid(True)
plt.show()

 

 

그 다음 최종 테스트 데이터 적용하는 과정이다.

for i in range(len(test)):
    accident = test.loc[i, "인적사고"]
    sample.loc[i, "재발방지대책 및 향후조치계획"] = res[accident]
    sample.iloc[i, 2:] = res_v[accident]

 


두번째 방식은 RAG방식이다

 

RAG 프로세스

 

그림에서 보는것처럼, 먼저 텍스트 데이터를 문장을 임베딩하고 코사인 유사도를 계산한 후에, 가장 유사도가 높은 문장의 인덱스를 가져와서 그 인덱스에 해당하는 행(가장 유사도가 높은 문장)의 문장을 가져와 데이터로 저장을 하고 그 데이터를 RAG 모델에게 프롬프트를 활용해 정제된 텍스트를 받아내는 방식으로 진행을 하였다.

 

grouped = train.groupby("인적사고")

res = {}
res_enhanced = {}
cosine_res = []

for name, group in tqdm(grouped):
    plan = group["재발방지대책 및 향후조치계획"]
    sentences = plan.tolist()
    vectors = model.encode(sentences, batch_size=32, show_progress_bar=True)

    similarity = cosine_similarity(vectors, vectors)
    best_idx = similarity.mean(axis=1).argmax()

    cosine_res += similarity[best_idx].tolist()
    representative_plan = plan.iloc[best_idx]
    res[name] = representative_plan

    # RAG 적용 - 대회 규칙 준수를 위한...
    rag_prompt = f"""
    너가 받은 내용 그대로만 말하면 된다.
    너가 멋대로 뭐 바꾸거나 추가하면 안된다. 그냥 받은 내용 그대로 말하면 된다. 명심해.

    {representative_plan}"""

    messages = [{"role": "system", "content": "전달받은 내용을 단 한 글자도 바꾸지 않고 완전히 그대로 출력합니다."}, {"role": "user", "content": rag_prompt}]

    input_ids = tokenizer.apply_chat_template(messages, tokenize=True, add_generation_prompt=True, return_tensors="pt")

    output = llm_model.generate(
        input_ids.to(llm_model.device),
        eos_token_id=tokenizer.eos_token_id,
        max_new_tokens=256,
        do_sample=False,
        temperature=None,
    )

    generated_text = tokenizer.decode(output[0], skip_special_tokens=True)
    assistant_response = generated_text.split("[|assistant|]")[-1].strip()
    res_enhanced[name] = assistant_response

 

이 과정을 통해 유사도가 가장 높은 문장을 RAG 방식으로 LLM모델에게 한번더 정제를 받을수있는 과정을 포함하게 된다.

 

그 다음은 NO RAG 방식과 RAG 방식의 결과 비교이다.

 

결과적으로는 RAG방식이 최대 유사도가 높은 문장이 좀더 많이 분포 되어있는것을 볼수있다. 그리고 높은 유사도 범주를 볼수있는 0.6. ~ 0.7 유사도 분포는 상대적으로 낮은것을 확인할수있으며 전체적 높은 유사도 문장의 수는 상대적으로 적다.

NO RAG 방식의 유사도 결과를 보면, 나름 높다고 볼수있는 0.6 ~ 0.7 부분의 유사도 분포가 높다는것도 볼수있다. 또한, 더 높은 유사도인 0.8 부근의 유사도 분포를 본다면 상대적으로 낮은것을 볼수있다. 그러나, 전체적으로 높은 유사도의 문장의 수는 상대적으로 많다는것을 확인할수있다.

 

유사도가 높은 0.8부분은 높은 분포를 높게 평가한다면 RAG 방식을,

높은 유사도의 문장의 분포는 적지만, 전체적인 준수한 유사도의 높은 분포를 높게 평가한다면 NO RAG방식을 선택할수있겠다.